Fix “Cannot perform a React state update on an unmounted component” warning

Aishwaryalakshmi Panneerselvam
5 min readAug 15, 2021

Every React developer would have experienced the following error when working with React. It happens if one performs a state update (call setState) on a component that has been unmounted. React warns us that this causes memory leaks. Although it is just a warning, it is necessary to fix it to avoid these kinds of memory leaks.

When does this error usually occur

  • You have an asynchronous request to a backend API, you click a button that triggers the API, but you were not patient enough to wait for the response and navigate to a different route.
  • You have an event listener, but you fail to clean it up when the component unmounts
  • You have some setTimeout or setInterval that gets called after a component has unmounted, and it calls some setState operation.

Cleanup the side effects correctly when the component unmounts. It may not be a problem at the initial stages or if the webpage is short-lived. But over time, the memory leaks can accumulate and will slow down the application.

For example, in one of the companies I worked for, the CEO wanted all the web apps to be kept open without refreshing or closing for a few days to see whether the app slows down because of memory leaks. In cases like this, if your application does not prevent memory leaks, this will significantly slow down the application.

In this blog, I will explain how to prevent these memory leaks when making asynchronous requests.

This problem can be fixed in two steps

  • Dispatch action only when the component is mounted

This hook makes an asynchronous request to a backend API using fetch.

If you look at the highlighted part at line number 53, the useFetch hook calls the useSafeDispatch with its reducer’s dispatch function. The return value is processed version of the same dispatch function.

Inspecting the useSafeDispatch hook further:

This simple hook creates a ref, sets it to true on the component mount and unset it on unmount. It returns a function that calls the reducer’s dispatch only if mounted.current is true.

I have also enclosed the return value with useCallback. It memoizes the function to avoid unnecessary re-render in the useFetch hook.

The useReducer’s dispatch is stable, but ESLint does not know about it during function calls, so I have specified dispatch in the dependency array.

One piece of advice is never to omit dependency array warnings from ESLint. If you don’t specify correct values in the dependency array, this produces stale closures.

Another point I would like to explain is, as you can see in the highlighted part, I have used useLayoutEffect instead of useEffect. The primary difference between the two is

React calls the useEffect’s callback only after it renders the component. The browser paint happens before calling the effect callback, so the user can see the changes without waiting for the callback to complete.

React calls the callback of useLayoutEffect immediately after it has performed all DOM mutations. Use this if you want to perform some DOM measurements or calculations. So the callback gets called immediately before the browser gets a chance to paint these changes. If you use useEffect in these scenarios, the browser first paints a value, then the callback gets called and, the new DOM mutations with the callback takes effect and may cause some flicker on the UI.

Generally, useEffect is handy in most of the scenarios for performance reasons. We don’t want to block browser paints with our callback code. But, if your code performs some manipulations with the DOM directly, then prefer useLayoutEffect in such cases.

In our example, I have used useLayoutEffect instead of useEffect because I want the mounted.currentto be set before performing any other state update in the reducer.

If you omit the useSafeDispatchcall in the useFetch hook, then you will get the famous unmounted error. Here is a code sandbox demo.

In this sandbox, view the demo in a new browser window. Throttle the network request with slow 3G. Go to the Search tab, type 1 in the text box and come to the Home tab, then on the console, you will see the memory leak error.

If you uncomment the call to useSafeDispatch, then you won’t see that warning. Does this mean we have prevented the memory leak? Nope.

We have just silenced React not to throw the warning, but we still have the memory leak issue because the code still fires the asynchronous API request.

This brings us to the second step of the process, which is using AbortController

  • We can prevent the API request from being processed using JavaScript’s AbortController API. Here is a modified version of useFetch with AbortController API.

Cancelling promise requests is a three-step process, as mentioned in the picture.

  1. Create an instance of AbortController
  2. Pass the signal property of the controller to the fetch function
  3. Call controller.abort on component unmount. This cancels the request in flight. But, if the promise has been resolved and if you reach line number 28 before calling controller.abort, then line number 15 does not have any effect.

Replace all the occurrences of useFetch in the code sandbox demo and replace them with useCancellableFetch. Now when you perform the same action, I mentioned above, then you will see the following in the Network tab on the browser console.

The request to 8 is cancelled, when I switched tabs.

AbortController also has good browser support except for IE, unfortunately.

One more feature in AbortController is, you can pass the same signal to multiple fetch calls and cancel them all together with just one call to abort.

const controller = new AbortController();
const { signal } = controller;

fetch(url1, { signal });
fetch(url2, { signal });

controller.abort(); -> This aborts both the fetch calls with one signal

You can also view the code used in the demo in the GitHub repository

--

--