← Back to the library
FrontendApplied · 7 min

A handler or timer keeps reading old state

“A counter in setInterval sticks at 1, a socket or keyboard listener acts on yesterday's filter, or a callback logs a value that the screen already shows as changed.”

THE PRINCIPLE

State is not a variable that changes; it is a constant per render. Long-lived callbacks need an updater, a ref, or an Effect Event, not a missing dependency.

FIRST MOVES

  1. Find where the callback was created and when it was registered; an effect with [] captures the first render forever.
  2. If the new state depends on the old, use the updater form: setCount(c => c + 1). The effect no longer needs count as a dependency.
  3. If an effect must react to a value, list it as a dependency and let cleanup re-subscribe; do not silence the lint rule.
  4. If an effect only needs to read the latest value without re-running, move that logic into useEffectEvent (stable since React 19.2) and keep it out of the dependency array.
  5. For values read outside React's model (imperative libraries, timers you manage yourself), store them in a ref and read ref.current at call time.

TOOLS: THEN → NOW

useRef plus a sync effect to read the latest valueuseEffectEvent for logic fired from effects; refs are still right for imperative code
Disabling react-hooks/exhaustive-depseslint-plugin-react-hooks at latest, which understands Effect Events

PATTERN SNAPSHOT

function Ticker({ step, onTick }: { step: number; onTick: (n: number) => void }) {
  const [count, setCount] = useState(0);
  // Reads the latest onTick without restarting the interval.
  const tick = useEffectEvent((next: number) => onTick(next));
  useEffect(() => {
    const id = setInterval(() => {
      setCount((c) => c + step); // updater: never reads a stale count
    }, 1000);
    return () => clearInterval(id);
  }, [step]);

  useEffect(() => { tick(count); }, [count]); // tick is not a dependency
  return <output>{count}</output>;
}

CLOSE THE AI. EXPLAIN THIS.

An interval callback created in useEffect(..., []) calls setCount(count + 1). Why does the counter stop at 1, and which of three fixes fits best?

HOW IT WORKS UNDERNEATH

SOURCES

Guide reviewed