DEV Community

Elijah Trillionz
Elijah Trillionz

Posted on • Updated on

 

Cleaning up Async Functions in React's useEffect Hook (Unsubscribing)

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
  }
}, []);
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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

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.

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.
Enter fullscreen mode Exit fullscreen mode

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);
}, []);
Enter fullscreen mode Exit fullscreen mode

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);
}, []);
Enter fullscreen mode Exit fullscreen mode

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;
}, []);
Enter fullscreen mode Exit fullscreen mode

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
  }
}, []);
Enter fullscreen mode Exit fullscreen mode

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);
}, []);
Enter fullscreen mode Exit fullscreen mode

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
}, []);
Enter fullscreen mode Exit fullscreen mode

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();
}, []);
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
joeattardi profile image
Joe Attardi

Using an isMounted approach is somewhat of an antipattern, whenever possible it's better to cancel the request (in the fetch example, you can use AbortController to cancel the request).

Collapse
 
tanth1993 profile image
tanth1993

yeah. I don't like using isMounted, but in React component this is maybe the common way to handle the unmount component

Collapse
 
elijahtrillionz profile image
Elijah Trillionz

Thanks 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.

Collapse
 
gnt profile image
gntsketches • Edited

Can you say more about why using the mounted variable is an antipattern? Thanks!

Collapse
 
nokternol profile image
Mark Butterworth • Edited

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>

Collapse
 
elijahtrillionz profile image
Elijah Trillionz

Thanks for sharing.

Collapse
 
gnt profile image
gntsketches

Can you link to where the React team documents that the closure variable is an anti-pattern? Thanks!

Collapse
 
nokternol profile image
Mark Butterworth • Edited

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.

Collapse
 
httpjunkie profile image
Eric Bishard

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!

Collapse
 
elijahtrillionz profile image
Elijah Trillionz

Thanks you very much.
Really encouraging.

Collapse
 
yazan_qarabash profile image
Yazan Qarabash

πŸ‘πŸ»πŸ˜

Collapse
 
elijahtrillionz profile image
Elijah Trillionz

Glad you liked it

Collapse
 
rudystake profile image
Info Comment hidden by post author - thread only accessible via permalink
RudyStake • Edited

I am not getting this , can you help me out to figure this one ?
best tantrik in Ranchi

Some comments have been hidden by the post's author - find out more

Visualizing Promises and Async/Await πŸ€“

async await

☝️ Check out this all-time classic DEV post on visualizing Promises and Async/Await πŸ€“