Stale closure

2 snippets in React

REReact

Dependency Arrays

RE · useMemo & useCallback
syntax
// All values from component scope used inside must be listed
useCallback(() => fn(a, b), [a, b]);
useMemo(() => compute(x), [x]);
example
function ChatRoom({ roomId, serverUrl }) {
  // Recreated only when roomId or serverUrl change
  const connect = useCallback(() => {
    const ws = new WebSocket(`${serverUrl}/rooms/${roomId}`);
    ws.onopen = () => console.log('Connected to', roomId);
    return ws;
  }, [roomId, serverUrl]);

  useEffect(() => {
    const ws = connect();
    return () => ws.close();
  }, [connect]);

  return <div>Chat: {roomId}</div>;
}

Note Missing a dependency leads to stale closures. Including an unnecessary one causes pointless recalculations. Use the exhaustive-deps lint rule (eslint-plugin-react-hooks) to catch mistakes automatically.

Stale Closures

RE · Common Mistakes
syntax
// PROBLEM: handler captures old state value
// FIX: use updater function or ref
example
function BrokenCounter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      // BUG: count is always 0 (captured from initial render)
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []); // empty deps = closure captures initial count

  return <p>{count}</p>; // stuck at 1
}

function FixedCounter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      // FIX: updater function always has the latest value
      setCount(prev => prev + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <p>{count}</p>; // increments correctly
}

Note Stale closures happen when a callback captures a state value that never updates. Common in setInterval, setTimeout, and event listeners set up in useEffect. Use the updater form of setState or store values in a ref.