Acting as a front-end developer can be challenging and rewarding, but also frustrating at times. One of the common issues that you may encounter is getting a warning like this:
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 does this warning mean and how can you fix it? In this blog post, I will explain the cause and the solution of this problem, and provide some sources for further reading.
The cause of the warning is that you are trying to update the state of a component that has been unmounted from the DOM. Think of it like trying to send a text message to a friend who has already left the conversation. Even though you hit “send,” your message won’t reach them because they’re no longer there. Similarly, if an asynchronous operation, like fetching data from an API, resolves after the component is unmounted, it tries to “send” the data to a component that no longer exists. For example, imagine you have a component that fetches some data when it mounts, and displays it in a table:
import React, { useState, useEffect } from 'react'; import axios from 'axios'; function DataComponent() { const [data, setData] = useState([]); useEffect(() => { axios.get('/api/data') .then(response => { setData(response.data); }) .catch(error => { console.error(error); }); }, []); return ( <div> <h1>Data</h1> <table> <thead> <tr> <th>Name</th> <th>Age</th> </tr> </thead> <tbody> {data.map(item => ( <tr key={item.id}> <td>{item.name}</td> <td>{item.age}</td> </tr> ))} </tbody> </table> </div> ); } export default DataComponent;
Now, suppose you have another component that renders the DataComponent conditionally based on some state:
import React, { useState } from 'react'; import DataComponent from './DataComponent'; function App() { const [showData, setShowData] = useState(true); return ( <div> <button onClick={() => setShowData(!showData)}> Toggle Data </button> {showData && <DataComponent />} </div> ); } export default App;
If you run this app and click the toggle button before the data is fetched, you will see the warning in the console. It’s like ordering food at a restaurant, leaving before it arrives, and then the waiter still tries to serve you at your empty table. In this case, the DataComponent is unmounted before the axios.get promise is resolved, and it still tries to call setData with the response data, even though the component is no longer there to receive it. This is a no-op, meaning it does not affect the UI, but it indicates a memory leak because the component is still holding a reference to the state setter function.
The solution to this problem is to cancel the asynchronous operation in a cleanup function that is returned from the useEffect hook. Imagine you’re watching a movie on a streaming service, but you decide to switch to another show before the current one finishes. The streaming service stops loading the rest of the movie to save bandwidth. Similarly, the cleanup function stops the asynchronous operation when the component is unmounted, preventing it from trying to update the state of a component that’s no longer there. To cancel a promise, you can use an axios cancel token, which is an object that can be used to cancel a request. Here is how you can modify the DataComponent to use a cancel token:
import React, { useState, useEffect } from 'react'; import axios from 'axios'; function DataComponent() { const [data, setData] = useState([]); useEffect(() => { // create a cancel token const source = axios.CancelToken.source(); axios.get('/api/data', { // pass the cancel token as an option cancelToken: source.token }) .then(response => { setData(response.data); }) .catch(error => { // check if the error is caused by cancellation if (axios.isCancel(error)) { console.log('Request canceled', error.message); } else { console.error(error); } }); // return a cleanup function that cancels the request return () => { source.cancel('Operation canceled by the user.'); }; }, []); return ( <div> <h1>Data</h1> <table> <thead> <tr> <th>Name</th> <th>Age</th> </tr> </thead> <tbody> {data.map(item => ( <tr key={item.id}> <td>{item.name}</td> <td>{item.age}</td> </tr> ))} </tbody> </table> </div> ); } export default DataComponent;
Now, if you run the app and click the toggle button before the data is fetched, you will not see the warning anymore. Instead, you will see a message in the console saying that the request was canceled. This means that the component is no longer trying to update the state after it is unmounted, and there is no memory leak.
This is one way to fix the warning, but there may be other ways depending on your specific situation. The main idea is to make sure that you cancel or abort any asynchronous operations that may resolve after the component is unmounted, and that you use a cleanup function to do so.
If you want to learn more about this topic, here are some sources that I recommend: