Update
Note that as of Axios v0.22.0
, the cancelToken
API is deprecated in favor of using a fetch-like AbortController
.
There are already various hooks available online for AbortController
so I won't recreate it here but at a high-level it's conceptually quite similar to the cancel token!
What's the problem?
When developing with React and updating state inside of components, you may have come across the following error before:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application.
While this error could pop up for various reasons, one common cause is attempting to update state within the callback of a network request after the component has been destroyed.
For example, imagine we have a modal (yes I know, modals are inherently problematic, but for a lot of us they're also unavoidable) that when opened makes a request for some super important data it needs to set in state and show to the user:
const Modal = () => {
const [importantData, setImportantData] = useState({});
useEffect(() => {
axios.get('/important_data')
.then((response) => {
setImportantData(response.data);
});
}, []);
return (
// JSX with important data
)
}
Note: While this post is about Axios specifically, the idea can be applied to other APIs, like fetch
.
This is nice and works swimmingly when the user opens the modal and keeps it open. But what if they close it while the request is still in process? Sure the component may be gone; however, that callback within .then()
is still hanging around waiting to get executed.
Assuming the component unmounts on close, this will cause the error noted above to occupy our console like the US occupying native land, since we'll be attempting to set our importantData
state within a component that no longer exists.
What can we do about it?
One solution to this issue is to ensure that whenever our component unmounts, we cancel our pending request.
"But our request is already gone!" I hear you say.. "How can we cancel it??" you yell in despair..
Never fear fellow dev frantically trying to finish a feature before their deadline, as the folks behind Axios have already responsibly built in cancellation functionality!
The idea is that we create a cancel token and send it along with our request, which allows for us to cancel said request whenever we like.
In our Modal
component, this would look something like the following:
const Modal = () => {
const [importantData, setImportantData] = useState({});
useEffect(() => {
const source = axios.CancelToken.source();
axios.get('/important_data', {
cancelToken: source.token
}).then((response) => {
setImportantData(response.data);
}).catch((error) => {
if (axios.isCancel(error)) return;
});
return () => source.cancel();
}, []);
return (
// JSX with important data
)
}
Notice now we're performing a few extra steps with our lil axios
. Before we send the request we now create a source
variable holding the result of axios.CancelToken.source
, which is like a reference that we can associate with our request.
Then, along with our request we send an extra piece of data, cancelToken
, containing our source's token.
However, this alone still doesn't accomplish our goal of cancelling on unmount!
So, we also make sure to return a function from our useEffect
that cancels our source's token, which by design will run when the component unmounts.
Also note that when we cancel a token, the pending promise is rejected, resulting in an error. If you don't handle this error, it will pop up in the console.
Conveniently, Axios also provides an isCancel
function which allows you to determine if an error returned from a request is due to a cancellation, which you can see above in our catch
block.
This is cool for one-off use cases, but realistically we're going to need to reuse this functionality in many components (and even many times in the same component). So let's make our own hook out of it!
Hook, line, something something..
import { useRef, useEffect, useCallback } from 'react';
import { CancelToken, isCancel } from 'axios';
/**
* When a component unmounts, we need to cancel any potentially
* ongoing Axios calls that result in a state update on success / fail.
* This function sets up the appropriate useEffect to handle the canceling.
*
* @returns {newCancelToken: function, isCancel: function}
* newCancelToken - used to generate the cancel token sent in the Axios request.
* isCancel - used to check if error returned in response is a cancel token error.
*/
export const useCancelToken = () => {
const axiosSource = useRef(null);
const newCancelToken = useCallback(() => {
axiosSource.current = CancelToken.source();
return axiosSource.current.token;
}, []);
useEffect(
() => () => {
if (axiosSource.current) axiosSource.current.cancel();
},
[]
);
return { newCancelToken, isCancel };
};
The hook, useCancelToken
, utilizes useRef
to store our cancel token source. This is so that our source remains the same in case of a more complex component where re-renders may occur while a request is being made.
Further, our hook sets up and exports a newCancelToken
function, which sets the ref's current value to the created source and returns the token itself, so the consumer can send it along with their request. Note, that this function is memoized via useCallback
, so that it can safely be added to a useEffect
dependency array without causing an infinite loop of rendering.
I like this approach as I don't think the person using this hook should have to deal with the source object at all. All they should have to do is send the token with the request and let the hook handle the rest!
Last but not least, we set up a useEffect
with the sole purpose of cancelling the current source's token on unmount.
Note, we also export isCancel
so the consumer can handle their request failure errors appropriately.
So, how would we use this in our Modal
component?
import { useCancelToken } from './hooks.js';
const Modal = () => {
const [importantData, setImportantData] = useState({});
const { newCancelToken, isCancel } = useCancelToken();
useEffect(() => {
axios.get('/important_data', {
cancelToken: newCancelToken()
}).then((response) => {
setImportantData(response.data);
}).catch((error) => {
if (isCancel(error)) return;
});
}, [newCancelToken, isCancel]);
return (
// JSX with important data
)
}
Now all we do is call our newCancelToken()
function when sending our request and check the potentially resulting error with isCancel
. We don't even have to set up a cleanup return function!
happy dance
*Note that calling newCancelToken
multiple times within the same component won't actually allow you to cancel multiple requests. For that you either need to call it once and pass the cancelToken
to each request (also storing it in a ref if the component might re-render) or tweak the hook to return the cancelToken
instead of the function, as Mostafa helpfully explains in their comment.
Conclusion
When developing it can be easy to forget about all the uncertainties and gotchas of real-world use.
Maybe the end user's network isn't as fast as the one we're developing on. Maybe they lose connectivity midway through using a feature. Maybe they didn't want to use said feature at all and navigate away from it / close it immediately. And so on and so on.
Thus, it's important to program a bit defensively and ensure we cover our bases. Using a cancel token for async routines is one such example.
Also - I wrote tooken
instead of token
way too many times while writing this. Also also - tooken > taken.
Top comments (10)
Hi, thanks for your article.
I believe that the current
useCancelToken
is faulty in some cases.Currently: it exports a
newCancelToken
method that generates a new cancel token on each call of this function, and saves the latest cancel token source object on a ref. This will cause a problem in a component that does 2 different api calls with 2 different tokens. This hook will only cancel the latest of them on unmount.i.e., a component that looks like this
I've reproduced this bug in the code in this repo here: github.com/MostafaOmar98/use-cance...
When clicking
unmount A
quickly (before allowing the api calls to finish),Expected result: logs to console "canceled 1", "canceled 2", "canceled 3" (order doesn't matter) with no warnings
Current result: it logs
canceled 3
,Done 1
,Done 2
, and spits out thesetState
warningThe suggested implementation is to create only one cancel token per component and cancel all api calls on unmounting:
Also, as an addition to the article. This hook only deals with cancelling api calls on unmounting. Another use case for cancelling is cancelling api calls on dependency changes. For example:
If the userId changes quickly, we might want to cancel the old ongoing request. I believe it is better to create the CancelToken/AbortController locally inside the effect and cancel/abort in the return function of the effect without the usage of extra hooks. Do you have another suggestion or pattern for doing this?
Thanks!
Hey Mostafa!
Thanks for the thorough reply and bringing up the issue about creating multiple cancel tokens within the same component. It's true that if you're going to be using it in several different calls like that you'd either want to just create one cancel token and export it directly (like you suggested) or create the cancel token once in the calling component (and storing it in a ref if it may rerender). I think your approach is the simplest as long as a function isn't specifically needed for some reason. I'll add a little note to the article pointing to your comment π
As for your other point about the use cases of the hook. Yes, this is only about unmounting. There are maaaany cases where you may want to cancel a request, which may not even require a dependency change (e.g. network issues and race conditions). For those cases I've typically just handled them manually like you suggested, as they tend to be more difficult to easily abstract than the classic "if component unmounts." If you come up with a better way though let me know!
Have a great one!
Thanks for the reply and the feature on the article! Have a great one too!
Maybe it's this plugin that will help you control the cancellation more easily.
π github.com/axios-use/axios-use-react. (A React hook plugin for Axios).
Lightweight, cancelable and less change.
I hope this is useful to you. Thanks~~~
Hi, thank you for the tutorial. One question from me.
If I have this code
api({
method: 'POST',
url: URL,
params: {
'user-token': Token,
},
});
*api is from axios instance
where should i put the cancelToken?
Hey there Diko!
In your code the cancel token would go within that same config object. So something like:
Note however that if you're using at least Axios
v0.22.0
the cancel token API has been deprecated in favor of a fetch-like abort controller.Thanks for the hook! This saved me
This is just a beauty, thank you so much!
Thanks a lot! I have been searching for this for so long. You explained very easily how to achieve it using a hook. Thanks again.
My pleasure! Glad it helped :)