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.

Discussion (10)

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 Author

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
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 Author

Thanks you very much.
Really encouraging.

Collapse
nokternol profile image
Mark Butterworth • Edited on

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 Author

Thanks for sharing.

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

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

Collapse
_yazan profile image
Yazan Qarabash

👍🏻😍

Collapse
elijahtrillionz profile image
Elijah Trillionz Author

Glad you liked it

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