Runnable lesson · press Try it on the examples to edit and run them here.
Some effects start something that keeps going: a timer, an event listener, a subscription. If you never stop it, it leaks. The cleanup function is how you stop it.
What a cleanup function is
An effect can return a function. React calls it:
- Before running the effect again (when its dependencies changed), to undo the previous run.
- When the component is removed from the screen (unmounted).
function Timer() {
const [seconds, setSeconds] = React.useState(0);
React.useEffect(() => {
const id = setInterval(() => setSeconds((s) => s + 1), 1000); // start
return () => clearInterval(id); // clean up
}, []);
return <p>Seconds: {seconds}</p>;
}
function App() {
return <Timer />;
}
Without clearInterval, removing the Timer would leave the interval running and calling setSeconds on a component that no longer exists.
Show and hide to see the cleanup
function Timer() {
React.useEffect(() => {
const id = setInterval(() => console.log("tick"), 1000);
return () => {
clearInterval(id);
console.log("timer cleaned up");
};
}, []);
return <p>Timer is running</p>;
}
function App() {
const [show, setShow] = React.useState(true);
return (
<div>
{show ? <Timer /> : <p>Timer stopped</p>}
<button onClick={() => setShow(false)}>Stop timer</button>
</div>
);
}
Press the button and look at the console panel: it prints timer cleaned up, and the ticking stops.
Event listeners
Always remove what you add:
function App() {
const [width, setWidth] = React.useState(window.innerWidth);
React.useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return <p>Window width: {width}px</p>;
}
Drag the corner of the preview frame to resize it and the number updates. The same function reference is used for add and remove; a new arrow function would not be removed.
Cleanup also runs between effects
When a dependency changes, React runs the old cleanup, then the new effect. For a chat room: leave the old room, then join the new one.
const log = [];
function App() {
const [room, setRoom] = React.useState("general");
const [, force] = React.useState(0);
React.useEffect(() => {
log.push("joined " + room);
force((n) => n + 1);
return () => { log.push("left " + room); };
}, [room]);
return (
<div>
<p>Joined room {room}</p>
<button onClick={() => setRoom("music")}>Go to music</button>
<p>Log: {log.join(", ")}</p>
</div>
);
}
After the click, the log shows joined general, left general, joined music: the cleanup of the old room ran before the new room’s effect.
Why effects seem to run twice in development
In Strict Mode (on by default in the Vite template), React runs this sequence once, on mount, in development only: effect, then cleanup, then effect again. It simulates the component being removed and added back, to check that your cleanup properly undoes your setup. If your effect and cleanup are symmetrical, the double run is invisible; if not, you see a bug: two timers, two subscriptions, two connections.
This is a feature that tells you about a missing cleanup. It does not happen in production. Try it:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
const events = [];
function Demo() {
const [, refresh] = React.useState(0);
React.useEffect(() => {
events.push("connect");
return () => { events.push("disconnect"); };
}, []);
return (
<div>
<p>Strict Mode demo</p>
<button onClick={() => refresh((n) => n + 1)}>Show log</button>
<p>Events: {events.join(" > ")}</p>
</div>
);
}
createRoot(document.getElementById("root")).render(
<StrictMode>
<Demo />
</StrictMode>
);
Press Show log and you will see connect > disconnect > connect. Remove StrictMode and run again to see the single connect. More in React Strict Mode.
Common mistakes
- No cleanup for timers and listeners. They keep running and cause memory leaks and “state update on unmounted component” situations.
- Different function references for
addEventListenerandremoveEventListener. - Trying to “fix” the double run by removing Strict Mode. Fix the cleanup instead.
- Cleanup that reads stale values. It sees the values of the render that created it, which is usually what you want.
Key points
- Return a function from the effect to undo what it started.
- It runs before the next effect and on unmount.
- The development-only double run in Strict Mode exists to expose missing cleanups.