Chimezie Innocent I am Chimezie, a Software developer based in Nigeria. I am highly skilled in Html, CSS, and JavaScript to build web-accessible and progressive apps. I'm also skilled with Facebook technologies like React for web, React Native for android apps, and tailwind CSS sometimes as my CSS framework. I write technical articles too.

Understanding React’s useEffect cleanup function

6 min read 1780

Understanding React useEffect Cleanup Function

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 start off 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, hopefully, by the end of this article, you should be able to use the cleanup function comfortably.

What is the useEffect cleanup function?

Just like the name implies, the 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 up after itself using the cleanup function.

The useEffect Hook is built in a way that we can return a function inside it and this return function is where the cleanup happens. The cleanup function prevents memory leaks and removes some 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 stated previously, the useEffect cleanup function helps developers clean effects that prevent unwanted behaviors and optimizes application performance.

However, it is pertinent 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 is an array
useEffect( callback, dependency )

Therefore, when our effect is dependent on our prop or anytime we set up something that persists, we then have a reason to call the cleanup function.

Let’s look at this scenario: imagine we get a fetch of a particular user through a user’s id, and, before the fetch completes, we change our mind and try to get another user. At this point, the prop, or in this case, the id, updates while the previous fetch request is still in progress.

We made a custom demo for .
No really. Click here to check it out.

It is then necessary for us to abort the fetch using the cleanup function so we don’t expose our application to a memory leak.

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:

Warning Error

To fix this error, we use the cleanup function to resolve it.

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

The cleanup is commonly used to cancel all subscriptions made and cancel fetch requests. Now, let’s write some code and see how we can accomplish these cancellations.

Cleaning up a subscription

To begin cleaning up a subscription, we must first unsubscribe because we don’t want to expose our app to memory leaks and we want to optimize our app.

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

In the above code, we set the variable isApiSubscribed to true and then use it as a condition to handle our success request. We, however, set the variable isApiSubscribed to false when we unmount our component.

Canceling a fetch request

There are different ways to cancel fetch request calls: either we use AbortController or we use 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 can go further and add an error condition in our catch so our fetch request won’t throw errors when we abort. This error happens because, while unmounting, we still try to update the state when we handle our errors.

What we can do is write a condition and know what kind of error we will get; if we get an abort error, then we don’t want to update the state:

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, state won’t update either.

So, let’s see how we can do the same using the Axios’ cancellation option, 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 the AbortError in AbortController, Axios gives us a method called isCancel that allows us to check the cause of our error and know how to handle our errors.

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 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 every render and handles fetch errors.

Here, we import the post component in 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;

Now, click the button and, before the posts render, click the button again (in another scenario, it might navigate to another page before the posts render) and we get an error 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:

Error Message From Updating The State Of An Unmounted Component

Now, to clear this error and stop the memory leak, we must implement the cleanup function using any of the above solutions. In this post, 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();
  }, []);
  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 then try 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.

If it’s an abort error, then we don’t need to update the state, else we handle the error:

// 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 fetch and the axios.isCancel() method when using Axios.

With that, we are done!

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

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Chimezie Innocent I am Chimezie, a Software developer based in Nigeria. I am highly skilled in Html, CSS, and JavaScript to build web-accessible and progressive apps. I'm also skilled with Facebook technologies like React for web, React Native for android apps, and tailwind CSS sometimes as my CSS framework. I write technical articles too.

7 Replies to “Understanding React’s useEffect cleanup function”

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

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

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

Leave a Reply