← Debugging Guides

阅读中文版本

React useEffect Stale Closure — Missing Dependency

Fix stale values in effects by listing every reactive value used inside, or by reading the latest value from a ref to avoid re-running.

intermediatedebuggingreacthooksclosure

Symptoms

  • An effect keeps using the initial value of a prop or state
  • A timer logs the same count every tick
  • The lint rule react-hooks/exhaustive-deps warns about a missing dependency

Possible causes

  • The dependency array omits a value read inside the effect
  • An empty dependency array is used with state that later changes
  • A callback captures an old value and is not recreated

Diagnosis steps

Read the exhaustive-deps warning

npx eslint src --rule '{"react-hooks/exhaustive-deps": "warn"}'

Interpretation

The rule names exactly which dependency is missing from the array.

Confirm staleness with a log

git diff src/hooks/useTicker.js

Interpretation

Logging the captured value each run shows whether it is the initial or latest value.

Root cause

Each render creates a new closure; an effect captures the values from the render in which it was created. If a used value is not in the dependency array, the effect keeps the old closure and reads a stale value.

Fix

List every reactive value used in the effect, or move the latest value into a ref and read ref.current inside; never disable exhaustive-deps without a documented reason.

Risk note

Adding a dependency that changes every render causes the effect to loop; stabilize the value with useCallback or useMemo.

Verify the fix

Run the scenario and confirm the effect reads the latest value; the lint rule should report no missing dependency.

Symptoms

This debugging guide addresses “React useEffect Stale Closure — Missing Dependency”. Start from the symptoms, follow the ordered diagnostics, and confirm each hypothesis before moving on; the fix and verification steps then close the loop.

Observed symptoms include:

  • An effect keeps using the initial value of a prop or state
  • A timer logs the same count every tick
  • The lint rule react-hooks/exhaustive-deps warns about a missing dependency

Reproduce and Diagnose

Reproduce the failure in isolation before changing any code. Work through the checks below in order; each one narrows the search space.

Step 1: Read the exhaustive-deps warning

npx eslint src --rule '{"react-hooks/exhaustive-deps": "warn"}'

The rule names exactly which dependency is missing from the array.

Step 2: Confirm staleness with a log

git diff src/hooks/useTicker.js

Logging the captured value each run shows whether it is the initial or latest value.

Likely contributors, in order of frequency:

  • The dependency array omits a value read inside the effect
  • An empty dependency array is used with state that later changes
  • A callback captures an old value and is not recreated

Root Cause

Each render creates a new closure; an effect captures the values from the render in which it was created. If a used value is not in the dependency array, the effect keeps the old closure and reads a stale value.

The Fix

List every reactive value used in the effect, or move the latest value into a ref and read ref.current inside; never disable exhaustive-deps without a documented reason.

Watch out: Adding a dependency that changes every render causes the effect to loop; stabilize the value with useCallback or useMemo.

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

  useEffect(() => {
    const id = setInterval(() => {
      setCount((c) => c + 1);   // functional update avoids stale count
    }, 1000);
    return () => clearInterval(id);
  }, []); // safe: no external value read

  return <span>{count}</span>;
}

// reading latest state without re-running
const latest = useRef(count);
useEffect(() => { latest.current = count; }, [count]);

Verify and Prevent

Run the scenario and confirm the effect reads the latest value; the lint rule should report no missing dependency.

Once verified, add a regression test that reproduces the original symptom so the failure cannot silently return.

This class of failure is usually environmental rather than pure logic, so capture the exact command, the environment variables, and the working directory in your bug report. Reproduce it in the smallest possible script, then widen the scope only once the minimal case is understood. A short written checklist for this failure mode saves hours the next time it appears.