Why do AI-generated React useEffect snippets often have stale closures?

The item shows a candidate an AI-generated React component that uses useEffect to set up a setInterval callback which references a state variable, with an empty dependency array. The candidate is asked to identify why the component displays incorrect data over time and pick the right fix. The question probes one of the most common categories of bug in AI-generated React code: stale closures, where a callback captures a state value at one render and then keeps referring to that captured value forever, even after later renders update the state.

What this question tests

The concept under test is the interaction between JavaScript closures and React’s render-on-state-change model, and the specific reason why empty dependency arrays in useEffect and useCallback cause stale-state bugs. The React documentation and the React team’s own writing have labeled this pattern an anti-pattern in nearly every official guide since hooks shipped in 2019, but AI training data still contains a long tail of pre-2020 tutorials that demonstrate the empty-array pattern without warnings, and AI-generated code often reproduces it.

The question targets candidates who recognize that React hooks have a specific contract — every value from the component scope used inside the effect must appear in the dependency array, except for refs and setters from the same hook — and that AI-generated snippets often violate it. The correct fix depends on the snippet: sometimes it’s adding the missing dependency, sometimes it’s restructuring with a ref or a functional setter, sometimes it’s lifting the state higher.

Why this is the right answer

The correct answer identifies the closure-capture issue and picks the structurally clean fix. Walking through the canonical case:

// AI-generated, buggy: captures `count` at mount, never updates
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count);  // always logs 0
      setCount(count + 1); // increments based on stale count
    }, 1000);
    return () => clearInterval(id);
  }, []);  // <-- empty deps array is the bug

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

The interval’s callback captures count at the time the effect ran (mount time, when count === 0), and the closure keeps referring to that same count value forever. The component re-renders, but the interval keeps logging 0 and keeps calling setCount(0 + 1), which produces 1 once and then no further increments because React bails out of identical state updates.

There are three idiomatic fixes. The first is to add count to the dependency array, which causes the effect to re-run on every count change — correct but expensive because the interval is torn down and re-created every tick:

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, [count]);

The second, usually preferred fix is the functional setter form, which receives the latest state as its argument and sidesteps the closure entirely:

useEffect(() => {
  const id = setInterval(() => {
    setCount((c) => c + 1);  // no `count` capture, uses latest
  }, 1000);
  return () => clearInterval(id);
}, []);  // legitimately empty: no component-scope values are read

The third fix uses a ref to hold the latest value when more than one piece of state needs to be read:

function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  useEffect(() => { countRef.current = count; });

  useEffect(() => {
    const id = setInterval(() => {
      console.log(countRef.current);  // always latest
    }, 1000);
    return () => clearInterval(id);
  }, []);
}

The senior-level review skill is recognizing the pattern quickly and picking the simplest fix that works in context: functional setter when only one piece of state is involved, ref when reads need to span multiple state slots, and full dependency-array participation when the effect’s setup genuinely depends on the value.

What the wrong answers reveal

The plausible wrong options each map onto a different gap in React-hooks fluency:

  • “The component is fine; React reconciles state updates.” This option treats React’s render system as magic that fixes any code structure underneath. It doesn’t: closures in callbacks capture values per-render, and React has no mechanism to retroactively update them. Picking this option suggests the candidate has not internalized how render and closure interact.
  • “Add setCount to the dependency array.” This is a partial fix that misses the underlying issue. setCount is guaranteed stable by React (the same function reference across renders), so adding it does nothing. The dependency the linter actually warns about is count, and adding count is the literal fix; better is the functional setter.
  • “Disable the react-hooks/exhaustive-deps lint rule.” This is the worst possible response. Disabling the rule hides the warning without fixing the bug. AI-generated snippets that include // eslint-disable-next-line on the dependency array are an immediate review flag.

How the sample test scores you

In the AIEH 5-question AI-Augmented JavaScript sample, this item contributes one of five datapoints aggregated into a single ai_js_proficiency score via the W3.2 normalize-by-count threshold. Binary scoring per item: 5 for the correct option, 1 for any of the three wrong options. With 5 binary items, the average ranges 1–5 and the level threshold maps avg ≤ 2 to low, ≤ 4 to mid, > 4 to high.

Data Notice: Sample-test results are directional indicators only. A 5-question sample can’t reliably distinguish between “spots stale-closure bugs in AI-generated React” and “got lucky on these specific items”; for a verified Skills Passport credential, take the full 50-question assessment.

The full assessment probes hook semantics, render scheduling, batching, concurrent features, refs vs state, and the specific gotchas (useEffect cleanup ordering, useLayoutEffect, strict-mode double-invocation) at depth. See the scoring methodology for how AI-Augmented JavaScript scores map onto the AIEH 300–850 Skills Passport scale.

  • useCallback dependencies and child memoization. The same closure-capture issue appears in useCallback when callbacks read state but list incomplete dependencies; the child component receives a callback whose body refers to stale values. Reviewing AI-generated useCallback is the same exercise as reviewing useEffect.
  • useReducer as an alternative. When several pieces of state need to be updated based on the latest values, a reducer often produces cleaner code than chained refs and functional setters. AI-generated code rarely reaches for reducers; suggesting them in review is a senior-level contribution.
  • React strict mode and double-invocation. In development with strict mode, React intentionally invokes effects twice to surface cleanup bugs and idempotence errors. AI-generated effects that aren’t idempotent (e.g., effects that subscribe without unsubscribing) trip strict-mode double-invocation; reviewing for this is part of the AI-augmented React loop.

For the broader AI-Augmented JavaScript lineup including the full 50-question assessment, see the tests catalog and frontend engineering interview prep. Hiring teams looking to evaluate React-hooks fluency at scale should explore hire and the AI fluency in hiring overview for how this dimension fits into broader engineering signals.


Sources

Try the question yourself

This explainer covers what the item measures. To see how you score on the full ai augmented javascript family, take the free 5-question sample.

Take the ai augmented javascript sample