跪拜 Guibai
← All articles
React.js

The Three Iron Rules of React State That Kill 90% of Beginner Bugs

By 小月土星 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

These three patterns are the root cause of most React re-render bugs and performance regressions. A developer who internalizes lazy initialization, updater functions, and derived state writes components that are both faster and immune to entire categories of stale-closure and out-of-sync UI defects.

Summary

State initialization in React runs on every render unless you pass a function to useState. A direct call like useState(heavyComputation()) wastes cycles because JavaScript evaluates arguments before React can discard them; wrapping it in an arrow function defers execution to mount time only.

State updates are asynchronous and batched, mirroring how a DocumentFragment collects DOM changes before committing them. Calling setState multiple times in the same handler reads the same closure snapshot, so the only way to chain updates correctly is to pass an updater function that receives the latest queued value.

The most reliable way to eliminate synchronization bugs is to not create state in the first place. Any value computable from existing state or props should be derived during render; a three-question test helps spot 'fake state' before it introduces inconsistency.

Takeaways
Passing a function call to useState runs that call on every render; pass an arrow function instead to make it a lazy initializer that only executes on mount.
Multiple setState calls in the same synchronous handler all read the same closure snapshot, so setCount(count + 1) three times increments by 1, not 3.
An updater function setCount(prev => prev + 1) receives the latest queued state, making it safe for sequential updates that depend on the previous value.
React batches state updates the way a DocumentFragment batches DOM insertions: multiple changes are collected and committed in a single render to avoid wasted work.
Any value that can be computed from existing state or props should be derived during render, not stored as a separate useState call.
Use a three-question test before calling useState: can it be computed from existing data? Does another state have to follow it? Does it have an independent lifecycle?
Conclusions

Framing React's batch updates as the framework-level equivalent of a DocumentFragment is a useful mental bridge for developers coming from vanilla DOM manipulation.

The advice to default to updater functions when unsure is a pragmatic rule that eliminates an entire class of closure-related bugs without requiring deep understanding of the batching mechanism.

Calling derived state 'the highest form of state management' reframes the goal from managing synchronization to eliminating it entirely, which is a more productive default mindset.

Concepts & terms
Lazy Initializer
A function passed to useState (or useReducer) that React calls only once during component mount to compute the initial state, avoiding expensive recomputation on every render.
Updater Function
A callback passed to a state setter (e.g., setCount(prev => prev + 1)) that receives the most recent queued state value rather than the value from the render closure, enabling reliable sequential updates.
Derived State
A value computed during render from existing state or props rather than stored in its own useState variable, eliminating the need to manually synchronize related pieces of data.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗