跪拜 Guibai
← All articles
Frontend · Interview

Closures and useState Aren't Two Topics: You Only Learned Half the Scope Chain

By BreezeJiang ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

A developer who memorizes closure definitions and React hooks as separate facts can still watch a counter freeze at 1 and have no explanation. Treating them as one scope-chain mechanism makes the failure mode obvious and the fix deterministic: reach for a functional update or put the value into the dependency array.

Summary

Interview prep usually files closures and React state under separate flashcards: define a closure, then memorize hook rules. The five demos in a Xiaomi frontend review outline put both on one chain. Lexical scope decides where a variable is looked up, starting from where the function is written, not where it is called. A closure exists when an inner function is carried outside its defining scope; the engine moves the variables it references into a Context object on the heap. The stack frame dies normally while the variables live on, which is why closure leaks grow the heap, and why the only leak question that matters is whether the holder is a global or local variable.

React follows the same model. A component is a function; each render is a fresh call with its own local snapshot. An effect with an empty dependency array creates its timer once in the first render, and the callback keeps reading `count = 0`. Calling `setCount(count + 1)` every second always computes `0 + 1`, React sees the same value, and the UI stays at `Count: 1`. Switching to `setCount(prev => prev + 1)` pulls the latest value from React's update queue instead of the stale snapshot. That same snapshot mechanism produces the `useState` rules: initial values run only on first render, updates are batched, and any update that depends on previous state has to use the functional form.

Takeaways
— Variable lookup starts at the definition position, not the call site; a top-level `bar` called inside `foo` still reads the top-level `myName`.
— Function nesting does not define a closure; carrying an inner function outside its defining scope and still reading that scope's variables does.
— An engine that sees a closure reference moves the referenced variables into a Context object on the heap, so the stack frame can be destroyed normally.
— Closures consume heap memory, not stack memory; stack overflow comes from runaway recursion.
— Every React render is a fresh component function call, and each one gets its own immutable snapshot of state.
— An empty-dependency `useEffect` creates its timer in the first render, so the callback's `count` is permanently 0.
— `setCount(count + 1)` inside that timer keeps computing `0 + 1`; React sees no state change and skips re-rendering.
— `setCount(prev => prev + 1)` fixes the stuck counter because React passes the latest queued value instead of the closure snapshot.
— `useState(() => heavy())` lazy initialization runs once; `useState(heavy())` recomputes on every render.
— `setState` only registers changes; React batches them into one re-render after the event handler finishes.
— Three consecutive `setCount(count + 1)` calls read the same snapshot and add only 1; the functional form accumulates.
— A closure leaks when a global variable holds it until page close; a local holder can be collected once its function is destroyed.
Conclusions

The same code starts misbehaving when `setInterval` becomes `useEffect`, which is the strongest clue that closures and React state are not separate topics: both fail at the boundary where a callback outlives the render that created it.

Heap-versus-stack wording changes the diagnostic: if closure variables stayed on the stack, a popped frame would wipe them and closures could not outlive their creator; naming the heap Context makes leak diagnosis a question of who still holds the closure.

A single yes/no test, whether the inner function gets carried out, explains heap persistence, a React counter stuck at 1, and global memory retention. That compression into three observable outcomes is what makes it useful in an interview.

Concepts & terms
Lexical scope
JavaScript resolves a variable name by where the function is written, not where it is called. A function defined at the top level looks up top-level variables even when invoked inside another function.
Closure
An inner function carried out of its defining scope that still accesses variables from that scope. The engine moves those referenced variables into a Context object on the heap so they outlive the outer function's stack frame.
React render snapshot
Each render calls the component function again and gives it a fresh copy of state values. A callback created in one render keeps reading that render's copy, even after later renders happen.
Functional update
A `setState` call that takes the previous state as its argument, such as `setCount(prev => prev + 1)`. React supplies the latest queued value instead of letting the callback read a stale value from the surrounding closure.
From the discussion
Featured comments
ShiliuX

The example of the counter getting stuck at 1 is explained very clearly: the callback in the empty dependency array keeps holding the count from the first render. The functional update line is indeed the most convenient.

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗