Skip to the solution
EasyPhone Screen6 min read

What is the purpose of the cleanup function in useEffect?

51 views

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.

  • hooks
  • lifecycle

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactA subscription whose setups and teardowns stay visibly balanced
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>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 26 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

What are React's design principles?

Continue