New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bug: React.StrictMode causes AbortController to cancel #25284
Comments
This is intentional behavior added in React 18 to StrictMode. Effects are now double-invoked (including their cleanup) to ensure component state is reusable. You can learn more about it in https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state |
@eps1lon I suspected as much. May I ask what the recommended approach is to manage the |
@EdmundsEcho the recommended approach it to just let it happen and make sure it works correctly |
The issue here is that your effect’s code isn’t symmetric. You shouldn’t implement cleanup in a separate effect — the effect that creates the controller should be the same one that destroys. Then an extra cycle wouldn’t break your code. |
Thank you for taking a look. I will review my code to see if that fixes the problem. To be clear, strict mode supports the use of the abort controller interface? Yes? With that confirmation I'll put in the work to have the app work in strict mode where now I'm getting errors (per your explanation for why). Thank you! |
UpdateDespite following the helpful advice (I learned a general good practice), I'm still having trouble getting the app the run in strict mode while using an abort controller. The app operation can be toggled with the inclusion of React.StrictMode. The console indicates how the fetch was cancelled. useEffect(() => {
// guard for calling with no args
const hasRequiredParams =
!blockAsyncWithEmptyParams ||
(blockAsyncWithEmptyParams && !isArgsEmpty(state.fetchArgs));
// this wont' be true when the cache change forces an update to the
// middleware function in this effect.
const changedFetchArgs = previousFetchArgsRef.current !== state.fetchArgs;
const isInitialized = state.fetchArgs !== null;
// Block the effect when triggered by changes in the cache.
// Required b/c the middleware function updates with the cache. So yes,
// re-run the useEffect, but block a repeated fetch.
if (hasRequiredParams && isInitialized && changedFetchArgs) {
(async () => {
//
const fetchArgs = state.fetchArgs === null ? [] : state.fetchArgs;
const augmentedArgs = useSignal
? [...fetchArgs, abortController.signal]
: state.fetchArgs;
try {
// ---------------------------------------------------------------------
// ⌛
const response = await asyncFn(...augmentedArgs);
if (
// 👍 avoid changing the cache when possible
// fyi: redirectData toggles whether to use the cache
!redirectData &&
compare(previousCacheRef, response, equalityFnName, caller, DEBUG)
) {
dispatch({ type: SUCCESS_NOCHANGE });
} else {
previousCacheRef.current = response.data;
// update reducer state
runMiddleware({
response,
consumeDataFn,
setCacheFn,
caller,
DEBUG,
});
}
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') {
console.warn(`use-fetch-api call was cancelle: ${caller}`);
} else if (e.name === 'CanceledError') {
console.warn(`fetch: ${e.message}`);
} else throw e;
}
})();
previousFetchArgsRef.current = state.fetchArgs; // used to limit effect that depends on args
}
// a ref to a callback that calls the abort controller abort function
return cancel;
}, [
runMiddleware, // guard against running the effect when changes
asyncFn,
consumeDataFn,
blockAsyncWithEmptyParams,
equalityFnName,
setCacheFn,
useSignal,
caller,
cancel,
dispatch,
state.fetchArgs,
redirectData, // bool
abortController, // hook
DEBUG,
]); |
I have the same issue with a single abort controller being called to abort multiple API calls.
According to API docs, this code should not have any issues and technically fetch, abort, then fetch again the data from API. But the issue is that the aborted API call throws CancelledError even on the second attempt:
|
I have same issue. Looks like useEffect has same signal from useState in both calls, because both get aborted. My react version is 18.2.0. This is my log: Code:
|
I am also having this issue.
|
Solution to this particular issue is to create abort controller in effect, and do not use useMemo or useState for it. But still, I consider this issue a bug, because it should not matter if we use AbortController or something else, the problem with useEffect getting wrong object still exists. |
I get this warning from eslint, so I believe I will continue to use the useMemo as suggested by the warning
|
I think your code is wrong, can you post it here? The code that you changed to useEffect only. |
This solution worked for me. |
I made a custom hook for the AbortController that seems to work in strict mode const useAbortController = () => {
const controllerRef = useRef(new AbortController())
const isMountedRef = useRef(false)
useEffect(() => {
const controller = controllerRef.current
isMountedRef.current = true
return () => {
isMountedRef.current = false
window.requestAnimationFrame(() => {
if (!isMountedRef.current) {
controller.abort()
}
})
}
}, [])
return controllerRef.current
} |
@BernaBoshnak You are creating an abort controller every time See: https://react.dev/reference/react/useRef#avoiding-recreating-the-ref-contents |
React version: 18.2
Steps To Reproduce
The current behavior
The "echoed" rendering of the component causes the the controller to go from aborted false -> true.
The expected behavior
I'm not sure if this is inherent to what react tests for in this mode, or something that can be expected to work.
The text was updated successfully, but these errors were encountered: