loading...
Cover image for An elegant solution for memory leaks in React

An elegant solution for memory leaks in React

nans profile image Nans Dumortier Originally published at nans-dumortier.com ・2 min read

Cover image from Valentin Petkov from Unsplash

An elegant solution for memory leaks in React


🔴 UPDATE

This "solution" doesn't seem to really avoid leaks, and I'm investigating for a true and elegant way to solve this. Even AbortController doesn't seem to be the silver bullet against memory leaks 😰.
Check out the discussion in the comments!


When working with asynchronous calls, for example API calls, you might have encountered this error :

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.

A GIF is worth a thousand words ...

A GIF showing a component that is being unmounted while it was performing some asynchronous logic

This is a small page that simulates some asynchronous logic on load, and then updates the view accordingly. Here, I unmount the component before the async work has been finished, and trigger the Error. (I took this example from this StackOverFlow post)

This is caused by this code :

function Example() {
  const [text, setText] = useState("waiting...");

  useEffect(() => {
    simulateSlowNetworkRequest().then(() => {
      setText("done!"); // ⚠️ what if the component is no longer mounted ?
      // => Warning: Can't perform a React state update on an unmounted component.
    });
  }, []);

  return <h2>{text}</h2>;
}

When running into that issue, I found multiple solutions, the most used one seems to be this one :

function OtherExample() {
    const [text, setText] = useState("waiting...");

  useEffect(() => {
    let isMounted = true; // 👈
    simulateSlowNetworkRequest().then(() => {
      if (!isMounted) { // 👈
        setText("done!"); // no more error
      }
    });
    return () => {
      isMounted = false; // 👈
    };
  }, []);

  return <h2>{text}</h2>;
}

But it requires you to add quite a lot of stuff into your component, having to deal with that isMounted variable all over the place ...

There are other interesting solutions, like making your Promises cancellable :

 You told me there would be an elegant solution !

I wasn't lying! The solution I came up with is a very simple hook. It works just like React's useState, but it basically checks if the component is mounted before updating the state !

Here is an example of the refactored code :

function OtherExample() {
  const [text, setText] = useStateIfMounted("waiting..."); // 👈

  React.useEffect(() => {
    simulateSlowNetworkRequest().then(() => {
      setText("done!"); // no more error
    });
  }, [setText]);

  return <h2>{text}</h2>;
}

Here is the CodeSandBox if you wanna play around !

TLDR

Use useStateIfMounted hook, that will only update the state if your component is mounted ! 🚀

I hope this might be helpful, feel free to reach me out in any case ! 🤗

Posted on by:

nans profile

Nans Dumortier

@nans

French software engineer, what's important to me is to keep learning. Every day.

Discussion

pic
Editor guide
 

I agree with the other replies (that this is just hiding the warning) however I’d call it a failure of React’s error messaging. A “memory leak” is too ambiguous, given React’s user group, so better to provide some common causes and solutions.

In this case, you’re making a request that will effect the component somehow; so you don’t want to handle the response if it comes after the component has unmounted. Rather than waiting for the request to complete and then checking whether the component is still mounted, you should instead cancel the request when the component unmounts.

  • The request should be made in a way that allows it to be imperatively cancelled.
  • The useEffect should return a function that cancels the request.
 

Yeah, thanks for your answer, I just went through some test, and must admit that this hook doesn't really solve the leak problem.
Here is a screenshot of the evidence :
a graph showing memory consumption over time, that grows

So you, Aleksandr and Xavier are right, thank you guys for pointing out!

I must say that I was mistaken when reading through this - kind of famous - lib's code. Being downloaded 17000+ times a week, I thought it would use good practices.

I'm going to update the documentation and the article to add a disclaimer about that, and in the meantime I will try to find another elegant solution.

 

To my surprise, we are getting the same kind of graph with an abort controller (I simply took the example that I posted in this article.
It seems that even cancelling the request causes a form of leak :
a graph with memory consumption going up
The main differences are :

  • the amount of listeners not going through the roof
  • however "Nodes" do
  • the numbers are different, but the memory consumption is still going up

For testing this out, I basically went on the app's page and ran :

const clickOnLink = (href) => document.querySelector(`a[href="/${href}"]`).click()
setInterval(() => clickOnLink("users"), 1000);
setTimeout(() => setInterval(() => clickOnLink("posts"), 1500), 500);

Then I performed an analysis over 2+ minutes

 

your explaination helped me so much when fixing this issue!. thanks

 
 

As far as I understood this article from React, this.isMounted() in class based component is an anti pattern, in this case we are in a function component, and the hook I built doesn't use any this.isMounted() method.
It basically uses a hook like so :

function useIsComponentMounted() {
  const isMounted = useRef(false);
  useEffect(() => {
    isMounted.current = true;
    return () => isMounted.current = false;
  }, []);
  return isMounted;
};

Did I missunsertand anything?

 

Yes, you did. What you just do is eliminating the warning, not solving anything. This is a good example of one of the worst attitudes to "solving" problems in programming. You will end up with a buggy program with no warning. Is that what you want?

Heya, please do not be rough, we're being professional and debating!
You are actually right, thank you for pointing out 😉. I wrote another answer here showing the memory consumption :

Yeah, thanks for your answer, I just went through some test, and must admit that this hook doesn't really solve the leak problem.
Here is a screenshot of the evidence :
a graph showing memory consumption over time, that grows

So you, Aleksandr and Xavier are right, thank you guys for pointing out!

I must say that I was mistaken when reading through this - kind of famous - lib's code. Being downloaded 17000+ times a week, I thought it would use good practices.

I'm going to update the documentation and the article to add a disclaimer about that, and in the meantime I will try to find another elegant solution.





and will (soon) take measures to ensure this is not misused.

All good. I vaguely recall reading about a future React feature a while back that was supposed to solve this issue of memory leaks, but I can't remember what it was called. Something about a new way to make API calls.

I'm trying to investigate for a better and if possible elegant solution, if by any chance you get one, please share it with me ! 😄

Yeah. I think the problem is much wider. There are so many misleading articles in this matter (canceling async request). For example '' JavaScript Promise is miraculously leakproof ", " There is no need for cleaning up after requests " - these are such common beliefs. I would call myself a newbie and I know how hard it is to trust to some resources

Yeah, I really feel deceived by all those articles and libs out there.
I must say I don't really know if there is a true good solution, since even AbortController doesn't seem to work ...
BTW I updated the article and invited the readers to come and check the discussions out 👍.

 

Have you already tried to use an asynchronous IIFE? Something like this:

useEffect (()=>{
  (async function Mount(){
      await action_1()
      await action_2()
    })();

   const unMount=()=>{}

   return unMount;
},[])

I usually use this approach on my useEffect hooks and I have no problems with memory leaks on my Components.

 

Thanks for your comment!
I don't think this would work, since if you update the state in action_1 or action_2 after the component has been unmounted, you will get the warning. Here is a codesandbox example :
codesandbox.io/s/trusting-mountain...
Or did I miss your point ?

 

I think cancel token of axios is an actual solution for this problem. What's your thoughts about request cancellation during useEffect cleanup?

 

As much as I apprieciate the "elegant" solution, this is not solving the root of the pbm which should be how to cancel the request when the app doesn't need the response anymore.

 

You are actually right, thank you for pointing out !
This is my mistake. I got confused by reading too much bad practices online 🙈
I wrote another answer here showing the memory consumption :

Yeah, thanks for your answer, I just went through some test, and must admit that this hook doesn't really solve the leak problem.
Here is a screenshot of the evidence :
a graph showing memory consumption over time, that grows

So you, Aleksandr and Xavier are right, thank you guys for pointing out!

I must say that I was mistaken when reading through this - kind of famous - lib's code. Being downloaded 17000+ times a week, I thought it would use good practices.

I'm going to update the documentation and the article to add a disclaimer about that, and in the meantime I will try to find another elegant solution.





and will take measures to ensure this is not misused.