The Three Iron Rules of React State That Kill 90% of Beginner Bugs
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.
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.
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.