React useEffect Cleanup Function
Learn what React memory leaks are and how to prevent them with the useEffect cleanup function.
Table of Contents 📖
- What is the useEffect Cleanup Function?
- useEffect Memory Leak Example
- How to use the useEffect Cleanup Function
- useEffect Cleanup Function and Dependencies
What is the useEffect Cleanup Function?
The useEffect cleanup function is an optional function returned from the useEffect hook that allows us to cleanup operations after a component has unmounted. For example, we can use the useEffect cleanup function to stop ongoing API calls from setting state on an unmounted component. Setting state on an unmounted component can cause memory leaks, meaning memory that is no longer needed is not released. This can result in performance issues or even program failure.
useEffect Memory Leak Example
As a demonstration, lets make an API call on initial component render with useEffect and then set the state of the component to the returned data with useState.
import { useEffect, useState } from "react";
const App = () => {
// Hold the data about posts
const [posts, setPosts] = useState([]);
// Make the network call
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts')
.then(resp => resp.json())
.then(data => {
setPosts([...data])
});
}, []);
// Return a list of the posts
return (
<>
<h1>My Posts</h1>
<div>
{posts.map(post => {
return (
<div key={post.id}>
<h2>{post.title}</h2>
<h4>{post.body}</h4>
</div>
)
})}
</div>
</>
)
}
export default App;
This works fine but if the component has unmounted before the API call finishes this can cause some issues. Specifically, React will try to set the component's state even though the component has unmounted, causing a memory leak.
How to use the useEffect Cleanup Function
To use the useEffect cleanup function, we simply return a function from within useEffect.
// Make the network call
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts')
.then(resp => resp.json())
.then(data => {
if (mounted) {
setPosts([...data]);
}
});
// Cleanup function
return () => {
}
}, []);
Now, lets prevent React from setting the state of the component if it has unmounted. We can do that with a simple boolean check.
// Make the network call
useEffect(() => {
let mounted = true;
fetch('https://jsonplaceholder.typicode.com/posts')
.then(resp => resp.json())
.then(data => {
if (mounted) {
setPosts([...data]);
}
});
// Cleanup function
return () => {
mounted = false;
}
}, []);
Here, when the component is unmounted the cleanup function is called which sets mounted to false. Therefore, the component's state will not be set after the component has unmounted.
useEffect Cleanup Function and Dependencies
The useEffect cleanup function runs when a component unmounts if an empty dependency array is provided to it. However, the cleanup function will run to cleanup the previous render if a non-empty dependency array is provided to it. This is useful because if useEffect makes an API call that depends on an item in its dependency array, we want to cancel the previous request when this item changes. As a demonstration, lets alter this program so that useEffect depends on the state of an id property. This id property will be used to fetch specific users from an API. In other words, when the id property changes, useEffect will be ran again using this new id.
import { useEffect, useState } from "react";
/**
* Click a button to fetch a random user.
* When clicking the button the Id changes which will
* cause the effect to run again and thus the cleanup
* function.
*/
const App = () => {
const [id, setId] = useState(1);
const [user, setUser] = useState({});
// Get a random id between 1 and 10
const getRandomId = () => {
const id = Math.floor(Math.random() * 10) + 1;
setId(id);
}
useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
.then(resp => resp.json())
.then(data => {
setUser(data);
});
return () => {
console.log('Cleanup!');
}
}, [id]);
return (
<>
<h1>Name: {user.name}</h1>
<h2>Email: {user.email}</h2>
<h3>Phone: {user.phone}</h3>
<button onClick={getRandomId}>Get Random User</button>
</>
)
}
export default App;
Here, the id changes each time the component's button is clicked, causing useEffect to be ran, causing the cleanup function to log Cleanup! to the console.
To make this more practical, lets use the useEffect cleanup function to abort the previous render's fetch request if it hasn't finished. In other words, if a new id is generated before the previous fetch request finishes, lets cancel the fetch request that is still in progress. We can do this with the AbortController interface.
useEffect(() => {
// Cancel the fetch request
const controller = new AbortController();
const {signal} = controller;
fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {signal})
.then(resp => resp.json())
.then(data => {
setUser(data);
});
// Cancel uncompleted request when Id changes
return () => {
controller.abort();
}
}, [id]);
The AbortController interface allows us to abort one or more web requests at any time. We associate the AbortController with an API request by passing it as an option to the fetch API. Specifically, it is the signal property of the AbortController interface that is used to cancel a DOM request. In the cleanup function, we call the abort method to cancel the fetch request before it has completed. Now, if the id changes before the request is complete the request will be aborted, saving us from more memory leaks.