Functional components in React are most beautiful because of React Hooks. With Hooks, we can change state, perform actions when components are mounted and unmounted, and much more.
While all these are beautiful, there is a little caveat (or maybe not) that is a little bit frustrating when working with useEffect hook.
Before we look at this issue let's do a quick recap on the useEffect hook.
Effect Hook
The useEffect hook allows you to perform actions when components mount and unmount.
useEffect(() => {
// actions performed when component mounts
return () => {
// actions to be performed when component unmounts
}
}, []);
The callback function of the useEffect
function is invoked depending on the second parameter of the useEffect
function.
The second parameter is an array of dependencies. You list your dependencies there.
So whenever there is an update on any of the dependencies, the callback function will be called.
useEffect(() => {
if (loading) {
setUsername('Stranger');
}
}, [loading]);
If the array of dependencies is empty like in our first example, React will only invoke the function once and that is when the component mounts.
But you may wonder, "what about when it unmounts, doesn't React call the function too"?.
Uhmmm no. The returned function is a closure and you really do not need to call the parent function (the callback function now) when you have access to the scope of the parent function right in the function you need (the returned function now).
If this isn't clear to you, just take out 7 mins of your time to take a look at an article on JavaScript closures I wrote.
So now we have gone through the basics as a recap, let's take a look at the issue with async functions.
Async functions in React
There is no doubt that you may have once used an async function inside the useEffect hook. If you haven't you are eventually going to do so soon.
But there is a warning from React that appears most times when we unmount and mount a component when we have an async function in the useEffect hook. This is the warning
If you can't see the image, here is the 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.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
The instruction is pretty clear and straightforward, "cancel all subscriptions and asynchronous tasks in a useEffect cleanup function". Alright, I hear you React! But how do I do this?
It's simple. Very simple. The reason React threw that warning was because I used a setState inside the async function.
That's not a crime. But React will try to update that state even when the component is unmounted, and that's kind of a crime (a leakage crime).
This is the code that led to the warning above
useEffect(() => {
setTimeout(() => {
setUsername('hello world');
}, 4000);
}, []);
How do we fix this? We simply tell React to try to update any state in our async function only when we are mounted.
So we thus have
useEffect(() => {
let mounted = true;
setTimeout(() => {
if (mounted) {
setUsername('hello world');
}
}, 4000);
}, []);
Ok, now we have progressed a little. Right now we are only telling React to perform an update if mounted
(you can call it subscribed or whatever) is true.
But the mounted
variable will always be true, and thus doesn't prevent the warning or app leakage. So how and when do we make it false?
When the component unmounts we can and should make it false. So we now have
useEffect(() => {
let mounted = true;
setTimeout(() => {
if (mounted) {
setUsername('hello world');
}
}, 4000);
return () => mounted = false;
}, []);
So when the component unmounts the mounted
variable changes to false and thus the setUsername
function will not be updated when the component is unmounted.
We can tell when the component mounts and unmounts because of the first code we saw i.e
useEffect(() => {
// actions performed when component mounts
return () => {
// actions to be performed when component unmounts
}
}, []);
This is how you unsubscribe from async functions, you can do this in different ways like
useEffect(() => {
let t = setTimeout(() => {
setUsername('hello world');
}, 4000);
return () => clearTimeout(t);
}, []);
Here is an example with an async function with the fetch
API.
useEffect(() => {
let mounted = true;
(async () => {
const res = await fetch('example.com');
if (mounted) {
// only try to update if we are subscribed (or mounted)
setUsername(res.username);
}
})();
return () => mounted = false; // cleanup function
}, []);
Update: As suggested by @joeattardi in the comments, we can use the AbortController
interface for aborting the Fetch
requests rather than just preventing updates when unmounted.
Here is the refactored code of the last example.
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
(async () => {
const res = await fetch('example.com', {
signal,
});
setUsername(res.username));
})();
return () => controller.abort();
}, []);
Now React will not try to update the setUsername
function because the request has been aborted. Just like the refactored setTimeout
example.
Conclusion
When I was still new in React, I used to struggle with this warning a lot. But this turned things around.
If you are wondering, "why does it only happen with async functions or tasks"? Well, that's because of the JavaScript event loop. If you don't know what that means, then check out this YouTube Video by Philip Roberts.
Thanks for reading. I hope to see you next time. Please kindly like and follow me on Twitter @elijahtrillionz to stay connected.
Top comments (13)
Using an
isMounted
approach is somewhat of an antipattern, whenever possible it's better to cancel the request (in the fetch example, you can useAbortController
to cancel the request).yeah. I don't like using
isMounted
, but in React component this is maybe the common way to handle the unmount componentThanks for this. I do know using the
mounted
approach is probably not the best way, but it's a way around it.I will try using the
AbortController
as suggested.Thanks.
Can you say more about why using the
mounted
variable is an antipattern? Thanks!Like the isMounted solution or not, this article is helpful. It sends the reader down a path of starting to understand the lifecycle (if we can call it that) and gets them thinking about the right way to do things. The comments also suggest some better ideas so all together, I think the reader walks away with more of an understanding. As well, you have done a good job at explaining some intermediate level hooks ideas. Very nice!
Thanks you very much.
Really encouraging.
I should start by saying I like the article and have yet to see anyone come up with an elegant solution to the problem. I agree that the closure variable is an anti-pattern and documented in the react teams blog as one of the most common. I solved this issue using signals.js to create an on-demand pub-sub which could be disconnected on unmount and personally I like the elegance of the execution path.
pseudo-code:
const useAsyncState = <TResult, TArgs>(promise: (...args: TArgs) => Promise<T>, cb: (v: T) => void): (...args: TArgs) => Promise<void> => {
const dispatcher = useMemo(() => new Signal<TResult>(), []);
useEffect(() => {
dispatcher.add(cb);
return () => dispatcher.clear();
}, [dispatcher, cb]);
return (...args: TArgs) => promise(...args).then(dispatcher.emit);
}
usage:
const [state, setState] = useState(undefined);
const loadData = useAsyncState(someAsyncMethod, setState);
useEffect(() => {
loadData();
}, [loadData]);
I wrote the code for an employer so cannot copy and paste it but above is the general idea/approach.
I actually like how the solution turned out and feel that it is clean albeit a little unintuitive as you then execute a Promise<void>
Thanks for sharing.
Can you link to where the React team documents that the closure variable is an anti-pattern? Thanks!
reactjs.org/blog/2015/12/16/ismoun...
I should also clarify that I am not a fan of the cancellable promise approach either. IMO, the dispatcher pub/sub link in a hook is the cleanest implementation I've seen to date.
The link above is not actually referring to a closure variable but the general approach of tracking mounted to avoid the condition is the reason ismounted() was cited as being removed and it forming an antipattern.
ππ»π
Glad you liked it
I am not getting this , can you help me out to figure this one ?
best tantrik in Ranchi
Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more