Demystifying useEffect's clean-up function

When you're starting to write React hooks, it can be hard to understand what's going on with useEffect, particularly if you're trying to translate Hooks to lifecycle methods in class components.

You read everywhere that you shouldn't compare useEffect to lifecycle methods, but then where do you start?

Thankfully, not knowing how useEffect's clean-up function works isn't as bad as getting the dependency array wrong, or passing constantly redeclared functions into useEffect.

That being said though, there are some nifty uses of the clean-up function that you should know about.

useEffect's clean-up function doesn't just run once

If you take nothing else away from this article, remember this: useEffect's clean-up function doesn't just run on unmount (assuming your dependency array isn't empty).

See this often overlooked sentence in the React Hooks API reference:

Additionally, if a component renders multiple times (as they typically do), the previous effect is cleaned up before executing the next effect

So when does clean-up run?

useEffect's clean-up runs after the next render, before the next useEffect.

This might mess with your brain a little bit, but check out this example:

import React, { useEffect, useState } from 'react';
export default function App() {
const [state, setState] = useState(null);
useEffect(() => {
console.log('I am the effect');
return () => {
console.log('I run after re-render, but before the next useEffect');
};
});
console.log('I am just part of render');
return (
<>
<button
onClick={() => {
setState('Some v. important state.');
}}
>
Click me
</button>
<p>state: {state}</p>
</>
);
}
This example is also available as a CodeSandbox.

When the above component first renders, in the console you see:

> I am just part of render
> I am the effect

If you then click the button (triggering a re-render), the following lines are printed underneath:

> I am just part of render
> I run after re-render, but before the next useEffect
> I am the effect

Even though the clean-up function is running in the new render, it still has the old prop values since it was declared in the previous render.

More concretely:

useEffect(() => {
console.log('id: ', id);
return () => {
console.log('id: ', id);
};
}, [props.id]);
  • id starts as 1.
  • Component renders, displaying id as 1 in the UI
  • useEffect runs, calling console.log and prints id: 1
  • Props change, setting id to 2
  • Component re-renders, displaying id as 2 in the UI
  • useEffect clean-up function fires, calling console.log and prints id: 1
  • useEffect runs, calling console.log and prints id: 2

What to actually use useEffect's clean-up functions for

Honestly, it's pretty rare that I find a use for useEffect's clean-up function in my day-to-day work as I don't use subscriptions at work (so I never need to unsubscribe from connections in the clean-up function).

You can use it to avoid race conditions in async requests, which is pretty nifty.

From Fixing Race Conditions in React with useEffect:

useEffect(() => {
let active = true;
const fetchData = async () => {
const response = await fetch(`https://swapi.dev/api/people/${props.id}/`);
const newData = await response.json();
if (active) {
setFetchedId(props.id);
setData(newData);
}
};
fetchData();
return () => {
active = false;
};
}, [props.id]);

You can also use an AbortController to cancel your requests instead of a boolean flag, see the example here.

Dedicated unmount hook

While React doesn't have a dedicated unmount hook, you can always use useEffect's clean-up function with an empty dependency array:

import React, { useEffect } from 'react';
const SomeComponent => () => {
useEffect(() => {
return () => {
// This code runs when the component unmounts
}
}, [])
// The rest of your component goes here.
}

I'm not sure why you'd want this (nor have I needed this), but it's worth knowing that it's possible.

(Shameless plug for the useEffect book I wrote below)

Tired of infinite re-renders when using useEffect?

A few years ago when I worked at Atlassian, a useEffect bug I wrote took down part of Jira for roughly one hour.

Knowing thousands of customers can't work because of a bug you wrote is a terrible feeling. To save others from making the same mistakes, I wrote a single resource that answers all of your questions about useEffect, after teaching it here on my blog for the last couple of years. It's packed with examples to get you confident writing and refactoring your useEffect code.

In a single afternoon, you'll learn how to fetch data with useEffect, how to use the dependency array, even how to prevent infinite re-renders with useCallback.

Master useEffect, in a single afternoon.

useEffect By Example's book cover