Understanding React’s useEffect cleanup function - LogRocket Blog (2024)

Editor’s note: This article was last updated by Eze Sunday on 8 February 2024 to include situations when a cleanup function is not necessary, as well as to explore how to make the useEffect function run only once.

Understanding React’s useEffect cleanup function - LogRocket Blog (1)

React’s useEffect cleanup function saves applications from unwanted behaviors like memory leaks by cleaning up effects. In doing so, we can optimize our application’s performance.

To follow along with this article, you should have a basic understanding of what useEffect is, including using it to fetch APIs. This article will explain the cleanup function of the useEffect Hook and, by the end of this article, you should be able to use the cleanup function comfortably.

What is the useEffect cleanup function?

As the name implies, useEffect cleanup is a function in the useEffect Hook that 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.

The 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])

Why is the useEffect cleanup function useful?

As previously stated, 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 arrayuseEffect(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, it is necessary for us to abort the fetch using the cleanup function. That way, we prevent these memory leak-related issues in our application.

When should we use the 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 send an error that looks like this:

Understanding React’s useEffect cleanup function - LogRocket Blog (2)

To fix this error, we use the cleanup function. According to React’s official documentation, “React performs the cleanup when the component unmounts. However, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time.”

As a side note before we continue: useEffects 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.

Over 200k developers use LogRocket to create better digital experiencesLearn more →

Cleaning up a subscription

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; };}, []);

Canceling a fetch request

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 prevent errors when aborting a fetch request. This error happens because, while unmounting, we still try to update the state when we handle our errors.

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 that error again 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.

How to use the useEffect cleanup function

Let’s see an example of when the above error 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 componentimport 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 every render 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 componentimport 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 an error 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 throws this error:

Understanding React’s useEffect cleanup function - LogRocket Blog (5)

Now, to clear this error and stop the memory leak, we must implement the cleanup function using any of the above solutions. We’ll use AbortController:

// Post componentimport 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 throws an error. As we discussed earlier, this error happens when we abort the fetch call.

useEffect catches the fetch error in the catch block and tries to update the error state, which then throws an error. 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 componentimport 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!

When a cleanup function is not necessary

Throughout 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 instance, if your useEffect has any of the following behaviors, you might not need to implement a clean-up function:

  • If your effect doesn’t perform any side effects like subscriptions, event listeners, or timers, there’s no need for cleanup. Effects that simply read data or update state without external connections usually don’t require the cleanup function
  • If your effect uses empty dependency arrays and does not depend on any service that requires closing or cleanup when the component unmounts or before the component that implements the useEffect re-renders

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

Conclusion

useEffect has two types of side effects: those that don’t need cleanup and those that do need cleanup like the examples we’ve seen above. It is vital we learn when and how to use the cleanup function of the useEffect Hook to prevent memory leaks and optimize applications.

I hope you find this article helpful and can now use the cleanup function properly.

Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to getan app ID
  2. Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, notserver-side

    • npm
    • Script tag
    $ 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> 
  3. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • NgRx middleware
    • Vuex plugin

Get started now

Understanding React’s useEffect cleanup function - LogRocket Blog (2024)
Top Articles
Latest Posts
Article information

Author: Rueben Jacobs

Last Updated:

Views: 5539

Rating: 4.7 / 5 (77 voted)

Reviews: 92% of readers found this page helpful

Author information

Name: Rueben Jacobs

Birthday: 1999-03-14

Address: 951 Caterina Walk, Schambergerside, CA 67667-0896

Phone: +6881806848632

Job: Internal Education Planner

Hobby: Candle making, Cabaret, Poi, Gambling, Rock climbing, Wood carving, Computer programming

Introduction: My name is Rueben Jacobs, I am a cooperative, beautiful, kind, comfortable, glamorous, open, magnificent person who loves writing and wants to share my knowledge and understanding with you.