跪拜 Guibai
← All articles
React · Hooks · Performance Optimization

A Single Input Box Exposes React's State Snapshot and Lazy Init Traps

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

These two `useState` behaviors cause real bugs and real lag in production forms and dashboards. Knowing that state is a snapshot eliminates a whole category of stale-closure confusion, and lazy initialization is a one-character fix that prevents O(n) work from running on every keystroke.

Summary

A user filtering demo ties together two React behaviors that trip up newcomers. State is a per-render snapshot, so logging a variable immediately after `setState` still shows the old value; the update only takes effect in the next render. The fix for consecutive updates is a functional updater that reads the pending previous state.

The same demo surfaces a performance trap. Passing a function call like `useState(heavyCalculation())` executes the heavy work on every render even though React discards the result after mount. Wrapping it in an arrow function turns it into a lazy initializer that runs exactly once.

Derived data, controlled inputs, and stable list keys round out the mental model. Filtered results are computed during render rather than duplicated into state, the input's value is driven entirely by React state, and `user.id` keys let React correctly reconcile list changes.

Takeaways
State is a per-render snapshot; `setState` schedules the next render but the current variable holds the old value until that render runs.
Use a functional updater (`prev => prev + 1`) when the next state depends on the previous one to avoid stale reads.
Data derivable from existing state or props should be computed during render, not duplicated into a separate state variable.
Controlled inputs pair `value` with `onChange` so React owns the input's value, making filtering, validation, and clearing straightforward.
`useState(expensiveCall())` runs the call on every render; `useState(() => expensiveCall())` runs it only on mount.
Stable keys like `user.id` let React track list items across filtering, additions, and removals; array indices break when the list order changes.
`performance.now()` wrapped around a function call turns a vague performance hunch into measurable milliseconds.
Conclusions

The lazy-initializer pitfall is easy to miss because React silently discards the recomputed value after mount, so the app works correctly but pays an invisible tax on every render.

Derived data is a design rule that eliminates an entire class of synchronization bugs: when you stop duplicating state, you stop needing to keep two copies in agreement.

The snapshot model is simpler than it sounds once you stop thinking of state as a mutable variable and start treating each render as a frame with its own frozen values.

Concepts & terms
State snapshot
In React, state is tied to a specific render. Calling `setState` schedules a future render with the new value, but the current function execution still sees the value from the current render's snapshot.
Lazy initializer
A function passed to `useState` (e.g., `useState(() => compute())`) that React calls only during the initial mount, avoiding expensive recalculations on subsequent renders.
Controlled component
A form element whose value is driven by React state via the `value` prop and updated through an `onChange` handler, giving React full ownership of the input's data.
Derived data
Values computed from existing state or props during render rather than stored separately, preventing duplicate state and synchronization bugs.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗