Skip to solution
easyPhone Screen

What is the purpose of the cleanup function in useEffect?

51 views
01

Understand the problem

Question presented to candidate: "What is the function you return from useEffect for, and exactly when does React call it?"

What a strong answer should cover:

  • It undoes what the effect set up — unsubscribe, clear a timer, abort a request, disconnect an observer.
  • The timing, stated precisely: it runs before the next effect run (when a dependency changed) and again on unmount. Never after the next run.
  • Each cleanup closes over the values from its own run, which is what makes it able to tear down the right thing.
  • Every setup has exactly one teardown — the counts always balance.
  • Why it matters: without it, each re-run leaks the previous subscription, and they accumulate.
  • Return nothing if there is nothing to undo — do not return a value, and never make the effect callback async, because that returns a promise where React expects a function.
  • StrictMode deliberately mounts, cleans up, and remounts in development so a missing cleanup fails immediately.
  • The cancellation-flag pattern for fetches is cleanup used to guard against a stale response, not just to free a resource.

Clarifying questions expected:

  • "Is the effect setting up something that persists — a listener, a timer, a request?" If not, it may not need cleanup at all.

Code / implementation expected: Yes — a subscription effect whose cleanup is visibly balanced against its setup.

hookslifecycle
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — assumes useEffect basics. Difficulty: Easy to Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The exact call sequence in section 3 was produced by render

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

A subscription whose setups and teardowns stay visibly balanced
Run Playground
import { useState, useEffect, useRef } from "react";

// A fake chat service so the connections are observable.
const live = new Set();
function connect(room, onLog) {
  live.add(room);
  onLog("connect: " + room);
  return {
    disconnect() {
      live.delete(room);
      onLog("disconnect: " + room);
    },
  };
}

function ChatRoom({ room, onLog }) {
  useEffect(() => {
    const conn = connect(room, onLog);
    // Cleanup closes over THIS run's room, so it always disconnects the room
    // it opened — never whatever the current room happens to be.
    return () => conn.disconnect();
  }, [room, onLog]);

  return <p style={{ margin: "6px 0" }}>Connected to <strong>{room}</strong></p>;
}

const ROOMS = ["general", "random", "support"];

export default function App() {
  const [room, setRoom] = useState("general");
  const [mounted, setMounted] = useState(true);
  const [log, setLog] = useState([]);
  const logRef = useRef((line) => {});
  logRef.current = (line) => setLog((l) => [...l, line]);

  // A stable callback so changing rooms is the only thing that re-runs it.
  const onLog = useRef((line) => logRef.current(line)).current;

  const connects = log.filter((l) => l.startsWith("connect")).length;
  const disconnects = log.filter((l) => l.startsWith("disconnect")).length;

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6 }}>
      {mounted ? <ChatRoom room={room} onLog={onLog} /> : <p><em>unmounted</em></p>}

      <p>
        {ROOMS.map((r) => (
          <button key={r} onClick={() => setRoom(r)} disabled={!mounted || r === room}
            style={{ marginRight: 6 }}>
            {r}
          </button>
        ))}
        <button onClick={() => setMounted((m) => !m)}>
          {mounted ? "unmount" : "mount"}
        </button>{" "}
        <button onClick={() => setLog([])}>clear log</button>
      </p>

      <p style={{ fontSize: 14 }}>
        connects: <strong>{connects}</strong> · disconnects: <strong>{disconnects}</strong> ·
        still live: <strong>{live.size}</strong>
      </p>

      <pre style={{ background: "#f6f6f6", padding: 10, borderRadius: 6, fontSize: 12, maxHeight: 160, overflow: "auto" }}>
        {log.length ? log.join("\n") : "(nothing yet — switch rooms)"}
      </pre>

      <p style={{ color: "#666", fontSize: 13 }}>
        Switch rooms a few times, then unmount. Every connect is followed by
        exactly one disconnect before the next connect, and "still live" always
        returns to 0 or 1 — never climbing.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 26 of 119 decoded in the React.js track. One more won't hurt.

Back to track