Deep Dive into React Fiber: How React Actually Reconciles Your UI

Frontend System Design Series — From virtual DOM diffing to commit mutations — a complete walkthrough of every phase and step React takes before a single pixel changes on screen.
Why This Topic Matters
You've probably said it in interviews: "React uses a virtual DOM and reconciles changes efficiently." But what actually happens between calling setState and the browser painting the updated UI?
The answer is React Fiber — the reconciliation engine powering React since v16. Understanding it explains why key props matter, why React.memo works, why hooks can't be called conditionally, and why Concurrent Mode exists.
The Big Picture: Two Phases
React's work is split into exactly two major phases:
┌────────────────────────────────────────┐
│ RENDER PHASE (async-safe) │
│ Interruptible · No DOM mutations │
│ beginWork → reconcileChildren → completeWork
└──────────────────┬─────────────────────┘
│ workInProgress tree ready
▼
┌────────────────────────────────────────┐
│ COMMIT PHASE (synchronous) │
│ Not interruptible · DOM changes here │
│ Before Mutation → Mutation → Layout │
└────────────────────────────────────────┘
Render Phase is pure computation — React figures out what changed without touching the real DOM. It's safely interruptible.
Commit Phase is irreversible — React applies all computed changes to the DOM in one synchronous pass. No interruptions allowed.
What Is a Fiber?
A fiber is a plain JavaScript object representing a unit of work. Every React element — component, DOM node, Fragment — has a corresponding fiber. It's React's internal representation of your component tree.
Key fields on a fiber:
tag— the type of fiber (FunctionComponent, ClassComponent, HostComponent likediv)pendingProps / memoizedProps— new vs last-committed propsmemoizedState— the hooks linked list (for function components) or class statechild / sibling / return— tree structure pointersstateNode— the actual DOM node (for host components)flags— pending work:Placement,Update,Deletionalternate— pointer to the other version of this fiber (current ↔ workInProgress)
React maintains two trees at all times:
current tree — what's rendered in the DOM right now
workInProgress tree — the tree being built during the render phase
This is the double buffering pattern. When commit completes, the workInProgress tree becomes the new current tree.
Trigger: How Reconciliation Starts
Reconciliation fires whenever React needs to update the UI — a setState call, useReducer dispatch, context value change, or a parent re-render. React schedules a render task and enters the render phase.
Phase 1: The Render Phase
The render phase is a depth-first traversal of the fiber tree. For each fiber, two functions fire: beginWork on the way down, completeWork on the way back up.
Step 1 — createWorkInProgress
Before any diffing, React clones the current fiber tree into a workInProgress copy. This is the scratch pad React edits freely — the current tree is never mutated directly. On the first render, fresh fibers are created. On subsequent renders, React reuses the previous workInProgress fibers.
Step 2 — beginWork
For every fiber, beginWork fires on the way down the tree. This is where your component function actually runs.
For a function component, beginWork does three things in sequence:
Bailout check — if props and context haven't changed (think
React.memo), skip re-rendering this fiber entirelyCall your component function — runs
renderWithHooksinternally, which processes all hooks in orderCall
reconcileChildren— diffs the returned JSX against existing child fibers
It then returns the first child fiber so the traversal can descend into it.
Step 3 — renderWithHooks (inside beginWork)
This is where every hook in your component runs. React sets the appropriate dispatcher (mount vs update), calls your component function, and resets the dispatcher afterward.
Hooks are stored as a linked list on memoizedState. Each call to useState, useEffect, useRef, etc. appends a node to this list in order. This is exactly why the Rules of Hooks exist — if you call hooks conditionally, the order of nodes in the list changes between renders, and React loses track of which hook corresponds to which state.
Step 4 — reconcileChildren
After your component runs, React compares the new JSX elements against the existing fibers. No DOM mutations happen here — React only writes flags onto fibers.
The diff algorithm uses two key heuristics that keep it O(n):
Heuristic 1: Elements of different types produce different trees. A <div> becoming a <span> tears down the entire subtree — React doesn't try to reuse it.
Heuristic 2: The key prop signals stable identity across renders. For lists, React first matches elements by key. Without keys, it falls back to positional matching — which is why removing an item from the middle of a keyless list causes every subsequent item to re-render.
The output of reconciliation is flags on fibers:
| Flag | Meaning |
|---|---|
Placement |
Insert this DOM node |
Update |
Update props/attributes on this DOM node |
Deletion |
Remove this DOM node |
PassiveEffect |
A useEffect needs to run |
LayoutEffect |
A useLayoutEffect needs to run |
Step 5 — completeWork
When a fiber has no more children to process, completeWork fires on the way back up the tree.
For host components (div, input, span):
On mount: creates the actual DOM node and assembles the subtree in memory — off-screen, not attached to the document yet
On update: prepares a prop diff (what attributes/styles need to change)
Bubbles flags upward so parent fibers know their subtree has pending work
After completeWork reaches the root, the entire workInProgress tree is ready — all flags set, all DOM nodes prepared in memory.
Phase 2: The Commit Phase
The commit phase is synchronous and uninterruptible. It has three sub-phases.
Step 6 — commitBeforeMutationEffects
Runs before any DOM mutations. Its job is to snapshot the current state of the DOM before it changes — this is where getSnapshotBeforeUpdate is called on class components, letting them capture scroll positions or dimensions before the DOM updates. It also schedules useEffect cleanups and setups to run asynchronously after paint.
No DOM changes happen in this step.
Step 7 — commitMutationEffects
This is where the DOM actually changes. React walks the fiber tree and processes every flag:
Placement— inserts the prepared DOM node (fromcompleteWork) into the correct position in the live documentUpdate— applies the prop diff to the existing DOM node (updates attributes, styles, event listeners)Deletion— removes the DOM node from the document, and recursively firescomponentWillUnmountanduseLayoutEffectcleanups on all descendants
After this pass, the DOM reflects the new UI. React then flips the root's current pointer — the workInProgress tree is now the current tree.
Step 8 — commitLayoutEffects
Runs synchronously after DOM mutations but before the browser paints. This is where:
componentDidMountandcomponentDidUpdatefire on class componentsuseLayoutEffectsetup functions runNew refs are attached
Because this runs before paint, useLayoutEffect is the right hook if you need to read layout (like getBoundingClientRect) and synchronously update state before the user sees anything. The tradeoff: it blocks the browser from painting until it completes.
Step 9 — Passive Effects (useEffect, async)
useEffect is deliberately not part of the synchronous commit. It's scheduled to run after the browser has painted, via a message channel task.
The order is always: cleanup from the previous render first, then the new setup. This ensures stale subscriptions or timers are torn down before new ones are created.
Full Flow at a Glance
setState / dispatch
│
▼
Schedule render
│
[RENDER PHASE]
createWorkInProgress
↓
beginWork (down)
→ renderWithHooks (run hooks)
→ reconcileChildren (diff, set flags)
↓
completeWork (up)
→ build DOM nodes off-screen
→ bubble flags to root
│
[COMMIT PHASE]
Step 6: commitBeforeMutationEffects (snapshot)
Step 7: commitMutationEffects (DOM changes)
root.current = workInProgress ✓
Step 8: commitLayoutEffects (useLayoutEffect, refs)
│
Browser paints 🎨
│
Step 9: flushPassiveEffects (useEffect, async)
Why Fiber Made This Interruptible
In the old stack reconciler (React 15), the render phase was a single recursive call that ran to completion. Deep trees could block the main thread for hundreds of milliseconds, causing dropped frames.
Fiber replaced recursion with an iterative work loop. Each beginWork / completeWork call is a discrete unit of work. Between units, React can check if higher-priority work has arrived — user input, an animation frame — pause, handle it, and resume. The commit phase stays synchronous because you can't partially apply a UI update without showing users a broken intermediate state.
This interruptible render phase is the foundation of every Concurrent Mode feature: useTransition, useDeferredValue, Suspense with streaming — all depend on it.
Key Takeaways
The render phase only computes — no DOM mutations, safely interruptible
The commit phase applies — synchronous, three sub-passes, not interruptible
Hooks are a linked list ordered by call sequence — hence the Rules of Hooks
keyprops are fiber identity in lists — wrong or missing keys cause unnecessary re-mountsuseLayoutEffectruns before paint (synchronous),useEffectruns after paint (async)Deleting a parent component automatically cleans up all descendant effects
Tags: #ReactJS #FrontendSystemDesign #WebPerformance #JavaScript #ReactFiber #FrontendEngineering

