From the ai augmented javascript sample test
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
setCountto the dependency array.” This is a partial fix that misses the underlying issue.setCountis guaranteed stable by React (the same function reference across renders), so adding it does nothing. The dependency the linter actually warns about iscount, and addingcountis the literal fix; better is the functional setter. - “Disable the
react-hooks/exhaustive-depslint 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-lineon 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.
Related concepts
useCallbackdependencies and child memoization. The same closure-capture issue appears inuseCallbackwhen callbacks read state but list incomplete dependencies; the child component receives a callback whose body refers to stale values. Reviewing AI-generateduseCallbackis the same exercise as reviewinguseEffect.useReduceras 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
- Abramov, D., & the React Team. (2024). useEffect — React Reference. Meta Open Source. https://react.dev/reference/react/useEffect
- Abramov, D. (2019). A Complete Guide to useEffect. overreacted.io. https://overreacted.io/a-complete-guide-to-useeffect/
- React Team. (2024). You Might Not Need an Effect — React Learn. Meta Open Source. https://react.dev/learn/you-might-not-need-an-effect
- Ecma International. (2024). ECMAScript 2024 Language Specification (ECMA-262, 15th edition). — Section 9.4 defines lexical environments and closure capture, the language-level mechanism behind React’s stale-closure behavior. https://tc39.es/ecma262/