跪拜 Guibai
← All articles
Frontend · React.js · Programming Languages

How React's useState, Fragment, and Lazy Init Map Directly to Native DOM Behavior

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

Understanding that React's abstractions are thin wrappers over native browser behavior—not magic—lets developers debug closure traps, avoid wasted re-computation, and reason about rendering without guessing. The DocumentFragment-to-Fragment mapping and the hooks linked-list walkthrough turn opaque framework behavior into inspectable mechanics.

Summary

Starting from raw `createElement` and `appendChild` calls, the progression moves through `DocumentFragment` as a batch-rendering container—an invisible wrapper that disappears after mounting its children—and then maps that pattern directly onto React's `<Fragment>` component. The same "memory vs. DOM tree" distinction underpins React's virtual DOM diffing.

The useState deep-dive unpacks why three consecutive `setCount(count + 1)` calls only increment by one: each call captures the same closure value, so React's update queue sees identical values and overwrites them. Switching to the functional updater `prev => prev + 1` chains the updates correctly because React feeds each function the previous state. The explanation then traces how the hooks linked list on the Fiber node stores state and why the call order must never change.

Lazy initialization gets the same treatment. `useState(heavyComputation())` runs the expensive function on every render because JavaScript evaluates arguments before the call; `useState(() => heavyComputation())` passes a function reference that React calls only once. The controlled-component breakdown follows a single keystroke from the native input event through React's synthetic event wrapper, into the state update queue, and back out to the DOM, showing exactly where React intercepts the browser's default behavior.

Takeaways
`DocumentFragment` batches DOM insertions in memory and disappears after mounting, exactly like React's `<Fragment>`.
`createElement` produces a real DOM node in memory, but the browser only paints it after `appendChild` attaches it to the tree.
Three consecutive `setCount(count + 1)` calls only increment by one because each captures the same closure value; React's update queue overwrites identical values.
Using the functional updater `prev => prev + 1` chains updates correctly because React passes the previous state into each function.
`useState(heavyComputation())` runs the expensive function on every render since JavaScript evaluates arguments before the call; `useState(() => heavyComputation())` defers execution to React, which calls it only on the first render.
A controlled input's `value` attribute is overwritten by React after the native input event fires but before the browser paints, making state the single source of truth.
Hooks must never appear inside conditionals or loops because React relies on call order to match each `useState` to its slot in the Fiber's linked list.
Derived data like `filteredUsers` is recalculated on every render and should not be stored in state; only the source data gets lazy-initialization protection.
Conclusions

The entire lesson is structured as a chain of "why" questions that force each React abstraction to justify itself against a native counterpart, which is a more durable mental model than memorizing API shapes.

Calling `setState` is synchronous enqueuing, not an async operation—the state value only updates on the next render, which explains why `console.log` immediately after always prints the old value.

React's synthetic event `e.target` still points to the real DOM node, so `e.target.value` reads directly from the browser's input; React only wraps the event object, not the target.

The lazy-initialization function runs at most once, so using it for non-deterministic values like `Math.random()` is pointless—the result is frozen from the first render.

Concepts & terms
DocumentFragment
A lightweight, invisible DOM container that exists only in memory. Child nodes can be appended to it without triggering browser reflows; when the fragment itself is appended to the live DOM, all its children are moved in a single operation and the fragment disappears.
Closure trap in useState
When a state variable is captured inside an event handler, its value is frozen to what it was during that render. Multiple setState calls in the same handler all see the same stale value, so passing a value directly overwrites rather than accumulates.
Functional updater
A function passed to a state setter (e.g., `prev => prev + 1`) that receives the most recent state as its argument. React chains these functions during a single render cycle, making each update build on the previous one.
Lazy initialization
Passing a function instead of a value to `useState` so that the expensive computation runs only on the component's first mount. On re-renders, React ignores the argument entirely and reads the stored state from the hooks linked list.
SyntheticEvent
React's cross-browser wrapper around native DOM events. It normalizes event properties and is pooled for performance, but its `.target` property still references the real DOM node, and `.nativeEvent` exposes the original browser event.
Hooks linked list
A singly linked list stored on each Fiber node that holds the state for every `useState` (and other hooks) in the order they were called. React relies on consistent call order across renders to match each hook to its stored state.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗