# 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:

```plaintext
┌────────────────────────────────────────┐
│        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 like `div`)
    
*   `pendingProps / memoizedProps` — new vs last-committed props
    
*   `memoizedState` — the hooks linked list (for function components) or class state
    
*   `child / sibling / return` — tree structure pointers
    
*   `stateNode` — the actual DOM node (for host components)
    
*   `flags` — pending work: `Placement`, `Update`, `Deletion`
    
*   `alternate` — 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:

1.  **Bailout check** — if props and context haven't changed (think `React.memo`), skip re-rendering this fiber entirely
    
2.  **Call your component function** — runs `renderWithHooks` internally, which processes all hooks in order
    
3.  **Call** `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 (from `completeWork`) into the correct position in the live document
    
*   `Update` — applies the prop diff to the existing DOM node (updates attributes, styles, event listeners)
    
*   `Deletion` — removes the DOM node from the document, and recursively fires `componentWillUnmount` and `useLayoutEffect` cleanups 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:

*   `componentDidMount` and `componentDidUpdate` fire on class components
    
*   `useLayoutEffect` setup functions run
    
*   New 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

```plaintext
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
    
*   `key` props are fiber identity in lists — wrong or missing keys cause unnecessary re-mounts
    
*   `useLayoutEffect` runs before paint (synchronous), `useEffect` runs after paint (async)
    
*   Deleting a parent component automatically cleans up all descendant effects
    

**Tags:** `#ReactJS` `#FrontendSystemDesign` `#WebPerformance` `#JavaScript` `#ReactFiber` `#FrontendEngineering`
