What does the cleanup function do in useEffect()?
It helps avoid memory leaks, duplicate subscriptions, unnecessary calls, and errors like
"Can't perform a React state update on an unmounted component".
Definition
A cleanup function is a function you return from
useEffect, which React calls automatically:
- before the component unmounts
- or before this effect runs again (if its dependencies changed)
Syntax
useEffect(() => {
// The effect body - runs on mount or update
return () => {
// Cleanup - runs on unmount or before a new effect run
};
}, [dependencies]);When exactly it's called
| Scenario | When cleanup fires |
|---|---|
| The component unmounts | Once, before it's removed from the DOM |
| An effect dependency changed | First the cleanup of the old effect, then a new run |
| The component re-rendered, but dependencies did not change | Cleanup is not called |
Example 1 - clearing a timer
useEffect(() => {
const id = setInterval(() => {
console.log('tick');
}, 1000);
return () => {
clearInterval(id); // cleanup on unmount
console.log('cleaned up');
};
}, []); // [] → the effect fires onceWhat happens:
- On mount, a timer is created.
- On unmount (when the component is removed), cleanup stops the interval.
Example 2 - unsubscribing from events
useEffect(() => {
const handleResize = () => console.log(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize); // cleanup
};
}, []);If you skip cleanup, the handler stays active even after the component is removed, causing a memory leak.
Example 3 - when dependencies change
useEffect(() => {
console.log('Running effect for user:', userId);
return () => {
console.log('Cleaning up before the new effect for user:', userId);
};
}, [userId]);What happens:
- When
userIdchanges, React first calls the cleanup of the old effect, then runs the new effect. - This matters for cancelling old requests, closing connections, and so on.
Example 4 - cancelling asynchronous requests
useEffect(() => {
const controller = new AbortController();
fetch(`/api/user/${userId}`, { signal: controller.signal })
.then(res => res.json())
.then(console.log)
.catch(console.error);
return () => {
controller.abort(); // cancel the request if the component unmounts
};
}, [userId]);If the user quickly switches userId, the old request is cancelled,
and the new effect starts from a clean state.
Why cleanup matters
Without cleanup functions, you can end up with:
-
Memory leaks (timers, subscriptions, WebSocket, listeners)
-
Duplicated effects on updates
-
Errors like:
"Can't perform a state update on an unmounted component"
Analogy in class components
| Function components | Class components |
|---|---|
return () => {...} inside useEffect() | componentWillUnmount() |
useEffect(() => {...}, [deps]) | componentDidUpdate() + componentWillUnmount() combined |
Mini behavior table
| Action | Effect runs | Cleanup runs |
|---|---|---|
| Mount | Yes | No |
| Update with changed dependencies | Yes | Yes (before it) |
| Update without changed dependencies | No | No |
| Unmount | No | Yes |
Summary
The cleanup function in
useEffect()is a mechanism for cleaning up side effects, which React calls before the component is removed or before the effect runs again.
It:
- frees up resources (timers, events, sockets),
- prevents leaks,
- guarantees the component always operates in a "clean" state.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.