Have you ever encountered that annoying warning in your console?
”Warning: Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application.”
I see this a lot when reviewing code from junior developers. It usually happens when we fire off an asynchronous task—like a data fetch or a timer—and then the user navigates away from the component before that task finishes. When the task finally tries to update the state of a component that no longer exists, React complains. And rightfully so!
Today, I want to talk about how we can prevent this using the Cleanup Function in useEffect.
What is the Cleanup Function?
In React’s functional components, useEffect is our go-to hook for handling side effects. But useEffect isn’t just about starting things; it’s also about stopping them.
The cleanup function is simply the function you return from your useEffect.
useEffect(() => {
// 1. The effect logic runs here
console.log("Component mounted or updated");
return () => {
// 2. The cleanup logic runs here
console.log("Component unmounted or before re-running effect");
};
}, []);
React runs this cleanup function when:
- The component unmounts.
- The component re-renders (before running the effect again).
A Real World Example: The Interval Timer
Let’s look at a classic scenario: a simple timer.
The Wrong Way
Here is a component that starts a timer to update a counter every second.
import { useState, useEffect } from "react";
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
console.log("Tick");
setCount((prev) => prev + 1);
}, 1000);
// Missing cleanup!
}, []);
return <div>Count: {count}</div>;
}
If I mount this Timer component and then immediately remove it from the DOM (e.g., navigate to another page), that setInterval keeps running in the background. It will keep trying to call setCount on a component that is gone. This is a memory leak.
The Right Way
To fix this, I just need to clear the interval when the component unmounts.
import { useState, useEffect } from "react";
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
console.log("Tick");
setCount((prev) => prev + 1);
}, 1000);
// The cleanup function
return () => {
clearInterval(intervalId);
console.log("Timer cleared!");
};
}, []);
return <div>Count: {count}</div>;
}
Now, when the component unmounts, React calls my cleanup function, clearInterval stops the timer, and my application stays clean and performant.
When Should You Use It?
I always recommend thinking about cleanup whenever you use:
- Event Listeners:
window.addEventListenerneeds a matchingremoveEventListener. - Timers:
setTimeoutandsetInterval. - Subscriptions: WebSockets or observables.
Conclusion
Handling side effects correctly distinguishes a junior developer from a senior one. It’s not just about making it work; it’s about making it robust.
So next time you write a useEffect, ask yourself: “Do I need to clean this up?” Your future self (and your users’ RAM) will thank you.