React's public model is UI = f(state): you describe the interface for a given state, and React updates the output when that state changes.
A render is not "React calling a component and updating the DOM." That skips the machinery behind stale closures, state jumping between list items, Effects running twice in development, and abandoned transitions.
React maintains its own representation of the interface. An update is scheduled, React computes a possible next tree during the render phase, and only then publishes it during the commit phase.
event, promise, or external store
→ enqueue an update
→ assign a priority
→ render and reconcile a possible next tree
→ commit the finished changes
→ let the browser paintRender purity, state snapshots, identity by type and key, and the render/commit split are public. Names such as Fiber, lanes, and beginWork describe the React 19.2 implementation. Application code should not depend on them.
The Fiber walk and interruptible rendering are visualized in Beyond The DOM's React Fiber and Concurrent Rendering in React 18.
1. JSX Produces Descriptions, Not DOM Nodes
JSX is syntax for describing UI. A transform turns it into a React element — a description, not a DOM node and not a Fiber.
function Greeting({ name }: { name: string }) {
return <h1 className="greeting">Hello, {name}</h1>
}{
type: "h1",
key: null,
props: {
className: "greeting",
children: ["Hello, ", name],
},
}- Elements are cheap, immutable snapshots. Creating one does not mount anything, run an Effect, or touch the DOM.
- A component element has a function as its
type. React later calls it while rendering. - Calling a component directly —
Greeting({ name: "Ada" })— bypasses identity and Hook bookkeeping. - React may create and inspect many elements for a render that never commits.
Three trees to keep separate:
React elements declarative descriptions returned by components
Fiber tree React's persistent bookkeeping and unit-of-work tree
Host tree DOM nodes, native views, or another renderer's output- The host tree depends on the renderer:
react-dom, React Native, React Three Fiber. React itself is renderer-agnostic. - A new element object does not mean a new DOM node. A component render does not mean a visible browser update.
- For React Native through Expo, see Understanding React Native in Depth.
2. Fiber Is React's Unit of Work
Elements are temporary descriptions. React needs a persistent structure that survives from one render to the next: the Fiber tree.
- A Fiber is a virtual stack frame for one component, host element, text node, or Suspense boundary. Host nodes get Fibers too — not only function components.
- Fibers use
child,sibling, andreturnlinks. That is a linked list, not an array of children. The next unit of work is always a pointer follow. - The old stack reconciler walked the tree by recursion on the JavaScript call stack and could not pause. Fiber walks with a work loop, so React can stop between units and resume from the saved pointer.
- The current Fiber is the tree on screen. Its
alternateis the work-in-progress Fiber where React computes the next state. - This is double buffering: mutate WIP privately, keep the current tree and DOM stable. Abandon work by throwing away WIP, not by mutating the screen.
- Fibers and subtrees can be reused. React does not clone the entire application from scratch.
{
type, // function, host tag, or other Fiber kind
child, sibling, return,
memoizedProps,
memoizedState,
alternate, // the other buffer
}App
child → Header
sibling → Main
child → Article
Article.return → Main
Main.return → Appcurrent tree work-in-progress tree
App ───────────── alternate ───────────── App
└─ List ──────── alternate ──────────── List
└─ Item ──── alternate ──────────── Item3. A State Setter Schedules Work
Calling a state setter does not mutate the state variable captured by the running function. It queues a value for a future render.
function Counter() {
const [count, setCount] = useState(0)
function increment() {
setCount(count + 1)
console.log(count) // Still 0 in this event handler
}
return <button onClick={increment}>{count}</button>
}- The
countbinding belongs to one invocation. The handler closes over that render's snapshot. - Observable model:
current state snapshot + queued updates → next state snapshot. - React batches updates so it can render once after a group of related setters. Batching is not merging every update into one value.
- Bail-out when the next value equals the current one (
Object.is) is an optimization, not a guarantee.
Updater functions matter when several updates depend on the previous result:
function addThree() {
setCount((value) => value + 1)
setCount((value) => value + 1)
setCount((value) => value + 1)
}Failure: three calls to setCount(count + 1) all capture the same count and all request 1. They do not form a sequence of +1 operations.
4. Lanes Represent Priority
Not every update is equally urgent. Typing should feel immediate; a large search result can wait.
React 19.2 represents priority with lanes — bits in a bitmask so several pending classes of work can exist at once.
urgent input update ─┐
default data update ├─ pending lanes on the root
transition update ┤
retry for Suspense ─┘A click typically occupies SyncLane — the implementation name for that urgent-input class, not something application code imports.
A long blocking render owns the main thread: the browser cannot paint, handle a click, or scroll until React finishes. That is the freeze Fiber and lanes exist to avoid — but only for renders that opt in.
Concurrent rendering is opt-in per feature. A default setState still renders as one uninterrupted transaction. startTransition, useTransition, useDeferredValue, and related Suspense behavior mark work that React may interrupt.
During a concurrent render, the scheduler can yield between Fibers (shouldYield in the implementation — a time slice on the order of a few milliseconds, not an API). React returns control to the browser, then resumes the same work-in-progress tree. If a higher-priority update arrives, it can throw that WIP away and restart.
- React may begin a transition, receive urgent input, discard the transition attempt, commit the urgent result, then restart the transition against the latest tree.
startTransitionmarks the React updates scheduled synchronously inside that function with transition priority.- It does not move JavaScript to another thread, make expensive computation free, or delay the function passed to
startTransition. useTransitionis the same mark plus anisPendingflag so you can show pending UI without hiding the last committed tree.useDeferredValueis the counterpart for a value you did not schedule yourself: keep showing the previous value while a heavier derived render catches up.- Lane names and yield budgets can change between versions. The durable lesson: React tracks which work is pending separately from which work should be attempted now.
function handleChange(value: string) {
setQuery(value)
startTransition(() => {
setFilter(value)
})
}The same urgent/transition split is the counter-and-heavy-chart demo: increment stays urgent; startTransition around setSeed lets the chart yield.
const [isPending, startTransition] = useTransition()
function regenerate() {
startTransition(() => {
setSeed(nextSeed())
})
}
return (
<button onClick={regenerate} disabled={isPending}>
{isPending ? "Updating…" : "Regenerate"}
</button>
)5. The Render Phase Computes a Candidate Tree
Rendering answers: "If these updates are applied, what should the tree look like?"
while next unit of work:
beginWork(fiber) // descend: invoke component, reconcile children
→ if child exists, that child is next
→ else completeWork(fiber) // ascend: prepare host, bubble effect flags
then sibling, or return to parent
concurrent and shouldYield? pause; resume this pointer laterThe visit order is still depth-first. The difference from a recursive call stack is that the loop holds the next Fiber in a variable, so it can stop between units.
beginWorkprocesses a Fiber's inputs. For a function component, this is where React invokes it and runs Hooks, then reconciles returned children. For a host element, it reconciles children without calling a component function.completeWorkruns after descendants finish. For host components, React can create or prepare DOM instances and bubble effect flags toward the root. The DOM is not mutated here.- The render phase must be pure because it is speculative. A concurrent render may yield, restart, render more than once, or throw the result away without committing. A default blocking render still runs this loop start to finish.
- Rendering may read props, state, context, and immutable snapshots. It may calculate and return elements.
- It must not send analytics, mutate shared objects, subscribe, or edit the DOM.
let nextId = 0
function Row() {
nextId += 1
return <li>Row {nextId}</li>
}Failure: the visible IDs now depend on how often React happened to call Row, including development checks and abandoned renders.
6. Reconciliation Decides Identity
React matches new child elements to existing Fibers by type, position, and key.
For a child at the same position:
- Same type and compatible key: reuse the Fiber and preserve its state.
- Different type: unmount the old subtree and mount a new one.
- Different key: a new identity even if the type matches.
function Profile({ editing }: { editing: boolean }) {
return editing ? <Editor /> : <Preview />
}Editor and Preview occupy the same position but have different types, so switching modes resets the subtree's state.
A key can intentionally reset state. <Editor key={documentId} /> is often cleaner than an Effect that clears every field.
{
todos.map((todo) => <TodoRow key={todo.id} todo={todo} />)
}When an item is removed, React walks the new element list against the current Fibers:
keys in the new list: 1, 3
current sibling list: 1 → 2 → 3
key 1 matches → reuse / clone that Fiber
key 2 is gone → mark a deletion flag (no DOM removal yet)
key 3 matches → reuse / clone that FiberThe missing Fiber is only flagged during render. The host node is deleted in the commit phase. That keeps the render phase pure and interruptible.
- Keys only need to be unique among siblings. They are not a normal prop.
- State does not live "inside the component function." React associates it with a Fiber's identity at a place in the parent tree.
Failure: an array index is safe only when identity really follows position. If a reorder changes which item owns index 0, React can keep the state at that index and attach it to the wrong data.
7. The Commit Phase Publishes the Result
When a render finishes, React has a Fiber tree and flags describing necessary changes. Committing is synchronous for a root. React does not show half of one commit and half of another.
before-mutation work
→ DOM mutations and deletions
→ current tree switches to the finished tree
→ refs and layout effects
→ browser gets an opportunity to paint
→ passive effectsgetSnapshotBeforeUpdatereads host information immediately before mutations.- Ref callbacks and
useLayoutEffectobserve the committed DOM before paint. Layout work blocks paint — reserve it for measurements that must happen before the user sees the frame. useEffectis passive and normally runs after paint.- Before React runs an Effect again, it runs the previous cleanup. On unmount, it runs the final cleanup.
- DOM changing and pixels appearing are not the same event. A long layout Effect can delay the paint of a commit whose DOM is already updated.
useEffect(() => {
const connection = connect(roomId)
connection.open()
return () => connection.close()
}, [roomId])8. Hooks Are Positional State on a Fiber
Hook state is stored by React, not in local JavaScript variables. A function component Fiber keeps Hooks as a linked list. The dispatcher walks that list in call order.
Profile fiber
memoizedState
→ useState hook
→ useReducer hook
→ useEffect hook
→ null- The first
useStatereceives the first state cell. The second Hook receives the second cell. - "Only call Hooks at the top level" preserves the sequence. The condition belongs inside the Hook.
- Each state Hook owns an update queue. Lower-priority updates can remain queued and be replayed later.
- Every render creates new handlers that capture that render's props and state. A "stale closure" is code from an older render running later.
- Omitting a reactive dependency does not freeze it at the newest value — it keeps the older closure.
useEffect(() => {
const id = setInterval(() => {
console.log(count)
}, 1000)
return () => clearInterval(id)
}, []) // This closure keeps the initial count.Failure: if a Hook sits behind if (editable), every Hook after it shifts position when editable changes, and React can no longer match cells.
9. Bailouts and Memoization Skip Work, Not Meaning
A Fiber can bail out when props, state, context, and lanes show that neither it nor its descendants need work at the current priority.
memoadds a props comparison at a component boundary.useMemocaches a calculation between committed renders while dependencies stay equal.useCallbackdoes the same for a function reference.- These are performance tools, not semantic guarantees. The calculation must still be correct if React recomputes it.
- A parent can render while a child does not. A component can render without its DOM changing.
The runtime decides whether a Fiber can bail out. React Compiler Internals is the build-time half: how HIR, effects, and reactive scopes automatically produce finer cache boundaries.
When investigating performance, keep three questions separate:
- Was an update scheduled?
- Which components performed render work?
- Which host changes and Effects were committed?
10. Suspense Turns Waiting into Render Control Flow
Suspense lets a render say that part of the tree is not ready. In React 19, use can read a Promise.
function UserProfile({
userPromise,
}: {
userPromise: Promise<{ name: string }>
}) {
const user = use(userPromise)
return <h2>{user.name}</h2>
}
function Page({ userPromise }: { userPromise: Promise<{ name: string }> }) {
return (
<Suspense fallback={<p>Loading profile...</p>}>
<UserProfile userPromise={userPromise} />
</Suspense>
)
}- If the Promise is pending, React suspends that path, finds the nearest Suspense boundary, and can render its fallback.
- Suspense does not fetch. A framework or cache must provide a stable Promise.
- Waiting belongs to the candidate tree. The committed tree does not have to disappear because another render is incomplete.
- For a non-urgent navigation, React can keep already visible content on screen while preparing the next state.
Failure: creating a fresh Promise during every client render can repeatedly suspend because every render sees a new resource.
11. Hydration Reuses Server HTML
Server rendering produces HTML, but HTML does not contain React state, event handling, or a client Fiber tree. Hydration builds the client representation while matching existing host nodes.
server
React tree → HTML stream → browser displays content
client
load JavaScript
→ create root work
→ match Fibers to existing DOM
→ attach event behavior
→ continue as an interactive React tree- Hydration is not "adding event listeners." React must reconstruct state and context, associate Fibers with host nodes, and verify compatible markup.
- A mismatch means server and client produced different descriptions for the same initial tree — clocks, random values, browser-only branches, locale differences, invalid HTML nesting.
- Make the initial render deterministic, pass a server snapshot, or render client-only information after hydration.
suppressHydrationWarningis a one-level escape hatch, not a general repair.- Suspense boundaries let React hydrate in sections so the entire page does not wait for one all-or-nothing render.
12. Strict Mode Tests Restartable Code
In development, Strict Mode may call render functions twice, run an extra setup-and-cleanup cycle for Effects, and re-run ref callbacks. These checks do not happen the same way in production.
- The purpose is to find code that breaks when rendering is restarted or synchronization is remounted.
- An impure render that mutates props exposes itself on the extra render.
- An Effect without cleanup exposes duplicate subscriptions. The solution is a cleanup that makes setup → cleanup → setup equivalent to one real setup.
- Code that is pure during render and symmetrical during Effect cleanup does not need to care how many speculative attempts React performs.
13. One Update, End to End
Without a concurrent feature, a heavy setState would run the work loop to completion on the main thread. The page could not paint or take the next keystroke until that render finished.
A user types into a search field. The app updates the input urgently and filters a large list in a transition.
- The handler calls
setQuery(urgent) andstartTransition+setFilter(transition). - React selects urgent work first, renders, reconciles, and commits the input DOM.
- The browser can paint the latest input value.
- React attempts the transition in interruptible slices. If another keystroke arrives, it can discard this WIP and commit the newer urgent input, then restart the filter against the latest tree.
- When a transition render completes, React commits the filtered list. Passive Effects run after.
- React never mutates the
querybinding inside an older handler. It creates new renders with new snapshots. - A partially rendered transition never leaks into the DOM. Only a completed commit changes the visible host tree. The last committed UI stays on screen, even if
isPendingis true.
The shortest accurate model:
Elements describe.
Fibers remember.
Queues collect.
Lanes prioritize.
Rendering computes.
Reconciliation matches.
Committing publishes.
Effects synchronize.When React behavior surprises:
- Which render created this callback? Its closure contains that render's props and state.
- Was the setter given a replacement or an updater? Repeated replacements may all use one snapshot.
- What is this component's identity? Parent position, type, and key.
- Is this code running during render or commit? Render must be pure.
- Did React render, commit, or both? A console log in a component does not prove the DOM changed.
- Could the work have been restarted? Strict Mode and concurrent rendering make this visible.
- Are all reactive Effect dependencies declared? If not, an older closure is probably being reused.
Common misconceptions:
- Fiber is not "the virtual DOM." Elements are descriptions; Fibers are the reconciler's persistent work and state.
- Fiber is a loop on a linked list, not recursion on the JavaScript stack. That is what makes pause and resume possible.
- Fiber does not make every render faster. Its benefit is prioritizing work and avoiding blocking the browser — and only for work that opts into concurrent rendering.
- Concurrent does not mean parallel, and it is not the default for every
setState. React still coordinates JavaScript on the main thread. Concurrent features let it interleave, yield, and discard obsolete renders. - Fiber is not React Three Fiber. One is the reconciler. The other is a Three.js renderer named after it.
For newer APIs, see What's New in React 19. Visualizations: React Fiber and Concurrent Rendering in React 18. Primary references: Andrew Clark's React Fiber architecture notes and React's docs on render and commit, state as a snapshot, preserving state, Effects, transitions, Suspense, and hydration.