Cleanup

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.

4 min read Level 2/5 #react#useEffect#cleanup
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:

  1. React runs the OLD cleanup (with the OLD captured values)
  2. 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 →