useEffect
cleanup functionEditor’s note: This article was last reviewed and updated by Joseph Mawa on 2 December 2024 and now covers what to do when the useEffect
cleanup function is unexpectedly called.
As the name implies, useEffect
cleanup is a function in the useEffect
Hook that saves applications from unwanted behaviors by cleaning up effects. It allows us to tidy up our code before our component unmounts. When our code runs and reruns for every render, useEffect
also cleans itself up using the cleanup function.
During a re-render, React calls the cleanup function to clean up effects from the previous render before calling the useEffect
callback. The dependencies you pass to the useEffect
Hook determine when it is called:
useEffect
Hook runs on mount and on every re-renderuseEffect
Hook runs on mount and runs on subsequent renders when one or all of its dependencies changeuseEffect
Hook is called once on mountThe useEffect
Hook is designed to allow the return of a function within it, which serves as a cleanup function. The cleanup function prevents memory leaks — a situation where your application tries to update a state memory location that no longer exists — and removes unnecessary and unwanted behaviors.
Note that you don’t update the state inside the return
function either:
useEffect(() => { effect; return () => { cleanup; }; }, [input]);
useEffect
cleanup function useful?The useEffect
cleanup function helps developers clean effects that prevent unwanted behaviors, thereby optimizing application performance.
However, it is important to note that the useEffect
cleanup function does not only run when our component wants to unmount — it also runs right before the execution of the next scheduled effect.
In fact, after our effect executes, the next scheduled effect is usually based on the dependency array:
// The `dependency` in the code below is an array useEffect(callback, dependency)
Therefore, when our effect is dependent on our prop or whenever we set up something that persists, we have a reason to call the cleanup function.
Let’s look at this scenario: imagine we request the server to fetch a particular user’s information using the user’s id
. Before the request is completed, we change our mind and try to make another request to get a different user’s information.
At this point, both fetch requests would continue to run even after the component unmounts or the dependencies change. This can lead to unexpected behavior or errors, such as displaying outdated information or attempting to update components that are no longer mounted. So, we must abort the fetch using the cleanup function.
useEffect
cleanup?Let’s say we have a React component that fetches and renders data. If our component unmounts before our promise resolves, useEffect
will try to update the state (on an unmounted component) and React will emit a warning that looks like this:
The above warning occurs in React versions 17 and below. It has been removed in React 18. It was removed because React has no way of detecting actual memory leaks. In versions 17 and below, React flags any state update after a component unmounts as a possible memory leak when most are not. More often than not, developers spend time coming up with solutions to remove false warnings like the one above.
Despite not getting the above warning in React 18 and above, as we will see shortly, you need to use the cleanup function to cancel subscriptions and other side effects that may otherwise cause memory leaks and, where necessary, cancel fetch requests to provide a better user experience. We will also explore workarounds to remove the above warning in React versions 17 and below.
According to React’s official documentation, “The cleanup function runs not only during unmount, but before every re-render with changed dependencies. Additionally, in development, React runs setup+cleanup one extra time immediately after component mounts.”
As a side note before we continue: useEffect
s can be made to run once by simply passing an empty array to the dependency list. When you provide an empty array as the dependency list for useEffect
, it indicates that the effect does not depend on any values from the component’s state or props. As a result, the effect will only run once, after the initial render, and it won’t run again for subsequent renders unless the component is unmounted and remounted:
useEffect(() => { // Effect implementation }, []); // Empty dependency array indicates the effect should only run once
Now that we understand how to make useEffect
run once, let’s get back to our cleanup function conversation.
The cleanup function is commonly used to cancel all active subscriptions and async requests. Now, let’s write some code and see how we can accomplish these cancellations.
To begin cleaning up a subscription, it is essential to first unsubscribe. This step prevents our application from potential memory leaks and aids in its optimization.
To unsubscribe from our subscriptions before our component unmounts, let’s set our variable, isApiSubscribed
, to true
, and then we can set it to false
when we want to unmount:
useEffect(() => { // set our variable to true let isApiSubscribed = true; axios.get(API).then((response) => { if (isApiSubscribed) { // handle success } }); return () => { // cancel the subscription isApiSubscribed = false; }; }, []);
There are different ways to cancel fetch request calls: either we use AbortController
or Axios’ cancel token.
To use AbortController
, we must create a controller using the AbortController()
constructor. Then, when our fetch request initiates, we pass AbortSignal
as an option inside the request’s option
object.
This associates the controller and signal with the fetch request and lets us cancel it anytime using AbortController.abort()
:
useEffect(() => { const controller = new AbortController(); const signal = controller.signal; fetch(API, { signal: signal, }) .then((response) => response.json()) .then((response) => { // handle success }); return () => { // cancel the request before component unmounts controller.abort(); }; }, []);
We enhance our error handling, and we can add a condition within our catch block to handle errors when aborting a fetch request.
By implementing a condition that identifies if the error is due to an abort action, we can avoid updating the state in such scenarios, ensuring smoother error management and component lifecycle handling:
useEffect(() => { const controller = new AbortController(); const signal = controller.signal; fetch(API, { signal: signal }) .then((response) => response.json()) .then((response) => { // handle success console.log(response); }) .catch((err) => { if (err.name === 'AbortError') { console.log('successfully aborted'); } else { // handle error } }); return () => { // cancel the request before component unmounts controller.abort(); }; }, []);
Now, even if we get impatient and navigate to another page before our request resolves, we won’t get a warning because the request will abort before the component unmounts. If we get an abort error, the state won’t update either.
So, let’s see how we can do the same using the Axios cancel token.
We first store the CancelToken.source()
from Axios in a constant named source, pass the token as an Axios option, and then cancel the request anytime with source.cancel()
:
useEffect(() => { const CancelToken = axios.CancelToken; const source = CancelToken.source(); axios .get(API, { cancelToken: source.token }) .catch((err) => { if (axios.isCancel(err)) { console.log('successfully aborted'); } else { // handle error } }); return () => { // cancel the request before component unmounts source.cancel(); }; }, []);
Just like we did with AbortError
in AbortController
, Axios gives us a method called isCancel
that allows us to check the cause of our errors and know how to handle them.
If the request fails because the Axios source aborts or cancels, then we do not want to update the state.
N.B., the Axios CancelToken
API is deprecated at the time of writing. It has been highlighted here for legacy reasons. Use the AbortController
API for new projects.
useEffect
cleanup functionLet’s see an example of when the above warning can happen and how to use the cleanup function when it does. Let’s begin by creating two files: Post
and App
. Continue by writing the following code:
// Post component import React, { useState, useEffect } from "react"; export default function Post() { const [posts, setPosts] = useState([]); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); const signal = controller.signal; fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal }) .then((res) => res.json()) .then((res) => setPosts(res)) .catch((err) => setError(err)); }, []); return ( <div> {!error ? ( posts.map((post) => ( <ul key={post.id}> <li>{post.title}</li> </ul> )) ) : ( <p>{error}</p> )} </div> ); }
This is a simple post component that gets posts on mount and handles fetch errors.
Here, we import the post component into our main component and display the posts whenever we click the button. The button shows and hides the posts, that is, it mounts and unmounts our post component:
// App component import React, { useState } from "react"; import Post from "./Post"; const App = () => { const [show, setShow] = useState(false); const showPost = () => { // toggles posts onclick of button setShow(!show); }; return ( <div> <button onClick={showPost}>Show Posts</button> {show && <Post />} </div> ); }; export default App;
Click the button and, before the posts are displayed, click it again. In a different scenario, this action could lead to navigation to another page before the posts appear, resulting in a warning message in the console.
This is because React’s useEffect
is still running and trying to fetch the API in the background. When it is done fetching the API, it then tries to update the state, but this time on an unmounted component, so it emits the following warning in React version 17 and below:
Now, to clear this warning, we must implement the cleanup function using any of the above solutions. We’ll use AbortController
:
// Post component import React, { useState, useEffect } from "react"; export default function Post() { const [posts, setPosts] = useState([]); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); const signal = controller.signal; fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal }) .then((res) => res.json()) .then((res) => setPosts(res)) .catch((err) => { setError(err); }); return () => controller.abort(); // clean up function }, []); return ( <div> {!error ? ( posts.map((post) => ( <ul key={post.id}> <li>{post.title}</li> </ul> )) ) : ( <p>{error}</p> )} </div> ); }
We still see in the console that even after aborting the signal in the cleanup function, the unmounting emits a warning. As we discussed earlier, this warning happens when we abort the fetch call.
useEffect
catches the abort error in the catch block and tries to update the error state, which then emits a warning. To stop this update, we can use an if else
condition and check the type of error we get.
In the case of an abort error, we don’t need to update the state. Otherwise, we handle the error accordingly:
// Post component import React, { useState, useEffect } from "react"; export default function Post() { const [posts, setPosts] = useState([]); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); const signal = controller.signal; fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal }) .then((res) => res.json()) .then((res) => setPosts(res)) .catch((err) => { if (err.name === "AbortError") { console.log("successfully aborted"); } else { setError(err); } }); return () => controller.abort(); }, []); return ( <div> {!error ? ( posts.map((post) => ( <ul key={post.id}> <li>{post.title}</li> </ul> )) ) : ( <p>{error}</p> )} </div> ); }
Note that we should only use err.name === "AbortError"
when using the Fetch API and the axios.isCancel()
method when using Axios.
With that, we are done!
useEffect
callsThe useEffect
Hook comes in handy for managing side effects in a React application. However, it is not uncommon for it to behave unexpectedly. Usually, this unexpected behavior is a result of incorrect usage, omitting certain dependencies, or incorrect usage of the cleanup function.
Therefore, if you face a situation where your useEffect
Hook behaves unexpectedly, you can do the following:
useEffect
Hook and are using it correctly for managing side effectsuseEffect
Hook takes a dependency array as a second argument. This array of dependencies determines when the useEffect
Hook is called. Check to make sure you are passing the correct dependenciesThroughout this guide, we’ve seen how you can use the cleanup function in the useEffect
Hook to prevent memory leaks and improve the performance of your application. However, in some cases, you might not need a cleanup function in useEffect
.
For example, if your useEffect
has any of the following behaviors, you might not need to implement a cleanup function:
useEffect
re-rendersHere is an example of when a cleanup function is not necessary in a useEffect
:
import { useEffect } from 'react'; function Page({ title }) { useEffect(() => { document.title = title; }, [title]); return <h1>{title}</h1>; }
In the above code, we don’t need a cleanup function even though there is an effect. The effect is self-contained and does not have a side effect that needs closing or cleanup and it’s been added to the dependency array. Moreso, if you don’t absolutely need a useEffect
, you should use other more suitable React Hooks.
In most cases, you want to useEffect
to interact with the outside world without interrupting the React rendering system and performance.
The useEffect
Hook in React is a powerful tool for managing side effects, but improper usage can lead to unexpected behaviors. To avoid issues like useEffect
being unexpectedly called or memory leaks, place useEffect
logically in your component structure (e.g., above or below functions as needed) and ensure dependencies are correctly managed.
Using cleanup functions effectively can prevent unwanted effects and improve your application’s performance. By understanding when useEffect
is called, you can confidently manage React component lifecycles and avoid common pitfalls.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
13 Replies to "Understanding React’s <code>useEffect</code> cleanup function"
well written.Thanks for clearly explaining
Thank you.
Wondering if I could get clarification on something that I can’t understand. With the isAPISubscribed example, you declare the isAPISubscribed var and set it to true, but in the clean up you set it to false. But when useEffect fires , isAPISubscribed is declared again and set to true. If it’s set to false in the clean up, but then it’s set to true when useEffect fires, wouldn’t the conditional that checks if it’s truthy always be met and it’s code block would always run? I’m just having a hard time understanding the use of declaring isAPISubscribed to false if it immediately gets set to true when useEffect updates. Thank you.
Okay Jamie, let me explain,
Firstly, I made an error there. It should be `let isApiSubscribed = true` and not const because const cannot be redeclared,
Secondly, it works just as how AbortController and CancelToken works just a different approach. The approach is that state will always and only update when our condition is true or checks for truthy but when our component unmounts, the condition is changed to false in the cleanup function. Remember, what we are trying to achieve is that when out component unmounts, state does not update.
Thirdly, concerning the part you find hard to understand. When our component unmounts, useEffect checks the cleanup function and if there is a code block there, executes it so when we set isApiSubscribed to false, useEffect therefore changes the condition to false and hence, state won’t update because state only updates when condition is true.
Hi, Chimezie. Thanks for responding. What I can’t wrap my head around is the value of isApiSubscribed during the time span of when the component unmounts and when the clean up function actually fires. The clean up function sets isApiSubscribed to false, but the clean up function only fires right before useEffect fires again. So wouldn’t that mean that during the time span of the unmount and remount that isApiSubscribed still true?
Say you got to a different page. During that time, isApiSubscribed is still true, which would cause the performance issues and leaks that you described. But when we go back to the page which has the useEffect that contains isApiSubscribed, that’s when the clean up function actually fires, right before useEffect is ran and thus setting isApiSubscribed to false.
When I log out the value of isApiSubscribed from within the clean up function it shows value of false, but it only logs it out right before useEffect runs again. So is isApiSubscribed set to false right when the unmount occurs, or when the clean up function actually runs?
Apologies for my confusion. Your article was written very well and my lack of understanding doesn’t come from your explanation, but from the concept of how the clean up function works behind the scenes. No worries if you don’t answer, btw.
Thanks bro
You are welcome Shinchan
thanks for sharing this blog . react native is now used as a framework for hybrid app development as well . its a good framework to work with for application development
Well detailed and explained. Thank you!
Fantastic ! such a nice and informative blog, I appreciate you taking the time and making the effort to create this content. Thank you for sharing.
Useful to note EffectCallback type, which provided by library. Return type of such effect function should be a cleanup function. Taking mentioned in account you should think about your effect -> what it should return to be used in useEffect like useEffect(wellWrittenFunctionEffectCallbackTyped, depsArr)
good
Great post! Thanks for putting this together. I find it very interesting and well thought out and put together. Keep it up.