Return a Function to Undo What the Effect Did
Cleanup
If your effect subscribes, opens, or starts something, return a cleanup function that unsubscribes, closes, or stops it.
What you'll learn
- Return a cleanup function from `useEffect`
- Clean up listeners, timers, and subscriptions
- Recognize when cleanup is required
Most effects “open” or “start” something — a listener, a timer, a subscription. If you don’t undo that work when the component unmounts (or before the effect re-runs), you’ll leak it.
The fix: return a cleanup function from the effect.
The Shape
useEffect(() => {
// set up
const id = setInterval(tick, 1000);
return () => {
// tear down
clearInterval(id);
};
}, []); React runs the cleanup:
- Before the effect runs again (deps changed)
- When the component unmounts
Event Listeners
useEffect(() => {
function onKey(e) {
if (e.key === "Escape") closeModal();
}
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [closeModal]); Without the cleanup, navigating away leaves a dead listener pointing into a removed component. Multiply by every modal mount and you’ve got a leak.
Subscriptions
useEffect(() => {
const unsubscribe = store.subscribe(state => setX(state.x));
return unsubscribe;
}, []); Returning the unsubscribe function directly works because most subscription APIs return one.
Timers
useEffect(() => {
const t = setTimeout(showToast, 3000);
return () => clearTimeout(t);
}, []); If the component unmounts before 3 seconds pass, the cleanup cancels the toast.
Fetch (Via AbortController, Two Lessons Forward)
For network requests, the cleanup story is more nuanced — you’ll see it in detail in the lesson on aborting.
What Doesn’t Need Cleanup
Pure side effects that complete instantly:
useEffect(() => {
document.title = `${count} unread`;
}, [count]); Setting document.title doesn’t open anything that needs closing.
Cleanup Order, Step By Step
Imagine an effect with [id], and id changes:
- React runs the OLD cleanup (with the OLD captured values)
- React runs the NEW effect (with the NEW values)
Each cleanup matches a specific run of the effect. That’s why listeners added inside an effect get removed inside its cleanup — they’re paired.
StrictMode Doubles It Up In Dev
<StrictMode> in development:
- Runs the effect
- Runs the cleanup
- Runs the effect again
This is to surface cleanups that were missing or broken. In production, effects run exactly once.
If a “second mount” causes a bug — like a duplicate websocket connection — that’s the bug, not StrictMode. Add cleanup.
Up Next
A common temptation is to use effects when you really wanted an event handler. Time to learn the difference.
Effects vs Events →