How to get rid of “Can’t perform a React state update on an unmounted component” OR Why it is important to cleanup useEffect

TL;DR

This post is about the importance of doing clean-ups in the React useEffect hook and is directed to beginners and everyone who wants to get a deeper understanding of useEffect.

Someone is cleaning a carpet from confetti with a vacuum cleaner.
Photo by No Revisions on Unsplash

Intro

If you ever built a website using React with functional components, chances are high that you encountered the useEffect hook. This hook synchs effects outside the React tree with state and props and can replace the lifecycle functions that were used in class-components, like compontentDidMount or componentDidUpdate. Even though it’s tempting to think that useEffect works the same way as these old functions, the concepts are quite different. Dan Abramov, creator of Redux and member of the React team, has written an extensive blog article on how useEffect works. If you want to deep-dive into the topic, I can highly recommend it. What I want to talk about in this post is one special behaviour of the useEffect hook that is often overlooked, especially by beginners.

Motivation

Maybe you have seen this warning in one of your projects:

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

A memory leak could cause problems like:

  • slowing down the application
  • performance issues due to reducing the amount of available memory
  • even a system crash

Obviously this should be avoided and React even suggests a fix:

> To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

But what exactly does this mean?

Live-Example

Let’s build a small example app to see what happened here. If you want to code along, I recommend setting up a React project using create-react-app.

$ npx create-react-app clean-up-example

Open the project in your favourite editor/IDE and select App.js.

Replace all the content of this file with:

import { useEffect, useState } from 'react';
function ChildComponent() { const [message, setMessage] = useState(""); useEffect(() => { new Promise((resolve, reject) => { setTimeout(() => { resolve("Time is up!"); }, 2000); }).then((value) => { setMessage(value); });
}, []);
return <div>Message: {message}</div>;}function App() { const [show, setShow] = useState(false); return ( <div style={{ height: "50vh", display: "flex", justifyContent: "center", alignItems: "center", flexDirection: "column", }} > <button onClick={() => { setShow(!show) }}> Show / Hide Message </button> {show && <ChildComponent />} </div> );}export default App;

To test your project, run it with:

$ npm start

Open http://localhost:3000/ in your browser. You should see the rendered button.

Button before click.

Let’s break down the code.

function App() {    const [show, setShow] = useState(false);    return (        <div            style={{                height: "50vh",                display: "flex",                justifyContent: "center",                alignItems: "center",                flexDirection: "column",            }}        >            <button onClick={() => { setShow(!show) }}>                Show / Hide Message            </button>            {show && <ChildComponent />}        </div>    );}

This is the main component of our app. It consists of a button that toggles a state variable. Is show set to true, another component will be rendered below the button.

function ChildComponent() {    const [message, setMessage] = useState("");    useEffect(() => {        new Promise((resolve, reject) => {            setTimeout(() => {                resolve("Time is up!");            }, 2000);        }).then((value) => {            setMessage(value);        });    }, []);    return <div>Message: {message}</div>;}

This component renders a message that is stored in a useState hook. The interesting part happens inside the useEffect hook. Let’s assume the message is a result of an asynchronous operation. E.g. this could be a GET request to an API that then responses with the message. Like any other asynchronous operation this will return a Promise which will eventually be fulfilled or rejected after some time. In our example this API call is simulated by an explicit creation of a Promise, which will resolve after two seconds and then update the state with the string “Time is up!”.

Button after click.

Make sure the app is running and click the Show / Hide Message button in the browser. You’ll notice that the message “Time is up!” appears after two seconds. Everything seems to work fine. Please open the developer tools of your browser. Now click the button again to hide the message section. Watch the console of the developer tools as you click the button to show the message but this time don’t wait the two seconds until the message is “received”. Instead click the button again immediately to hide the section. After two seconds this warning pops up in the console:

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

What went wrong? We re-rendered the ChildComponent by setting show to true. After the browser painted the component, useEffect gets triggered. Inside the hook a new Promise is created, but before it resolves, the component is already unmounted again by clicking the button and setting show to false. But the asynchronous operation was started and will resolve after two seconds and then try to set the state of the unmounted component with its resolved value. This leads to “Can’t perform a React state update on an unmounted component.” Gladly, there is an easy fix for this.

The first idea that may come to mind is to cancel the asynchronous operation. But that doesn’t work. A Promise, once returned, can’t be “stopped”. It will be fulfilled or rejected, no matter what. Instead we take a look at code after the Promise resolved. Here we can introduce a condition:

}).then((value) => {    if(...) {        setMessage(value);    }});

We create a Boolean isMounted that indicates if the component is in a mounted or unmounted state:

useEffect(() => {    let isMounted = true; // Add Boolean    new Promise((resolve, reject) => {        setTimeout(() => {            resolve("Time is up!");        }, 2000);    }).then((value) => {        if(isMounted) { // Condition            setMessage(value);        }    });}, []);

useEffect is triggered after the component is painted by the browser. But how do we set isMounted to false when the component unmounts? Here the useEffect clean-up comes into play. Just return a function:

useEffect(() => {    let isMounted = true;    new Promise((resolve, reject) => {        setTimeout(() => {            resolve("Time is up!");        }, 2000);    }).then((value) => {        if(isMounted) {            setMessage(value);        }});    // Clean-up:    return () => {        isMounted = false;    }}, []);

Now everything works as expected.

Recap

Data fetching or any other asynchronous operation returns a Promise. If the fulfilled Promise triggers a state update on an unmounted component, this will lead to a warning (or worse). To avoid this we added a condition to the state update. As soon as the component unmounts, the clean-up function inside useEffect will make sure that this condition will evaluate to false. Now when the Promise resolves there won’t happen a state update on the unmounted component.

Please let me know what your thoughts on this post and if you would be interested in a whole series about useEffect.

--

--