Back

React Fiber & Reconciliation

May 19, 2026 (2mo ago)

A deep dive into how React decides what changed, why Fiber is a linked tree of work, and what actually happens between render and commit.

Fiber tree with child and sibling pointers

Most tutorials introduce React as a UI library. That is correct — and incomplete.

Under the hood, React is closer to a UI runtime: a system that takes descriptions of what the screen should look like and turns them into updates against a host tree (DOM, native views, and so on). Two ideas make that runtime practical:

  1. Reconciliation — figuring out what changed
  2. Fiber — representing that work as units that can pause, resume, or be thrown away

This post is about those internals. It will not teach you how to build a todo app. It might help you understand why keys matter, why remounts reset state, and why React can stay responsive while a large tree updates.


Note: If you are learning React, start with the docs. This is a deep dive aimed at people who already use React and want a clearer mental model of what it does between setState and paint.


Host trees and descriptions

Some programs output numbers. React programs usually output a tree that may change over time.

That tree lives in a host environment outside React — most often the DOM. React’s job is to keep that host tree in sync with what your components describe.

The description itself is not the DOM. JSX compiles to plain objects: React elements.

// JSX is syntax sugar for objects like this.
// <button className="blue" />
{
  type: "button",
  props: { className: "blue" },
}

An element is a snapshot of UI intent. It has no persistent identity. The next render throws it away and creates a new one. That is intentional: you describe the UI for this moment, and React figures out how to get there.

What reconciliation is for

If React blew away the DOM on every update, you would lose focus, selection, scroll position, and a lot of performance.

So React compares the previous tree with the next description and applies the smallest practical set of host operations. That comparison process is reconciliation.

React does not solve the general tree-edit distance problem. That would be too expensive for UI. Instead it bets on two assumptions that are usually true for interfaces:

  1. Different types mean different trees. A <div> becoming a <span> at the same position is treated as a replacement, not a clever morph.
  2. Keys identify siblings. Among children of one parent, key tells React which previous child corresponds to which next child.

When type and key match, React can reuse the existing fiber (and usually the host instance and local state) while updating props and walking children.

// Fragile when items can move or be inserted
{todos.map((todo, index) => (
  <TodoItem key={index} todo={todo} />
))}
 
// Stable identity across renders
{todos.map((todo) => (
  <TodoItem key={todo.id} todo={todo} />
))}

Without stable keys, React falls back to position. Inserting at the front can attach old state to the wrong item and force unnecessary DOM work. With keys, React can move nodes, preserve state, and mark only the real placements and deletions.

Keyed list reconciliation: delete, reuse, place

Why Fiber exists

Before Fiber (React 16), reconciliation was recursive. A deep update could hold the main thread until the whole tree finished. Input felt janky. Animation stuttered.

Fiber reimplements the work as a linked tree of units, not a recursive call stack.

Each fiber is roughly one component (or host node) plus bookkeeping. The important pointers are:

That shape lets React walk the tree iteratively: go down through child, across through sibling, and back up through return. Because the “stack” is data, React can stop after a time slice, handle higher-priority work, then continue — or discard an in-progress render entirely.

A deliberately simplified fiber looks like this (Fiber is an implementation detail, not a public API):

type Fiber = {
  tag: number;
  type: unknown;
  key: string | null;
  pendingProps: unknown;
  memoizedProps: unknown;
  memoizedState: unknown;
 
  return: Fiber | null;
  child: Fiber | null;
  sibling: Fiber | null;
  alternate: Fiber | null;
 
  flags: number;
  subtreeFlags: number;
  lanes: number;
};

During an update it helps to think in terms of two trees:

alternate pairs a reused fiber with its twin in the other tree. After a successful commit, the finished WIP tree becomes current.

The two phases

Render phase vs commit phase

Render (reconcile)

React walks the WIP tree and compares each fiber to the next React element. Conceptually, one unit of work looks like this:

// Abridged from ReactFiberWorkLoop.js
function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate;
  const next = beginWork(current, unitOfWork, renderLanes);
 
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
 
  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

beginWork usually descends into a child. When there is nothing left to begin, completeUnitOfWork finishes the fiber, moves to a sibling, or climbs via return.

During this phase React may:

Importantly: the render phase does not mutate the DOM. That is why concurrent rendering can discard an in-progress tree when a more urgent update arrives.

Commit

Once the WIP tree is complete, React commits synchronously:

  1. before mutation — snapshot reads (for example getSnapshotBeforeUpdate)
  2. mutation — apply host inserts, updates, and deletes indicated by flags
  3. layout — run layout effects (useLayoutEffect, componentDidMount / DidUpdate)
// Schematic ordering — not React's full commitRoot
function commitFinishedTree(root, finishedWork, lanes) {
  commitBeforeMutationEffects(root, finishedWork, lanes);
  commitMutationEffects(root, finishedWork, lanes);
 
  root.current = finishedWork;
 
  commitLayoutEffects(finishedWork, root, lanes);
  schedulePassiveEffects();
}

Modern React walks flags and subtreeFlags on the finished tree. It no longer depends on the older linear “effect list.” Passive effects (useEffect) run in a separate phase — usually after paint, though React may flush them earlier in some cases.

A small walkthrough

function Counter() {
  const [n, setN] = useState(0);
 
  return (
    <button onClick={() => setN((value) => value + 1)}>
      clicked {n}
    </button>
  );
}
  1. A click schedules an update on the Counter fiber (a lane / priority).
  2. React clones work into the WIP tree and runs Counter with the next state.
  3. The <button> fiber keeps its identity; the text child is marked for an update.
  4. When the root finishes rendering, React commits the text mutation and makes the finished tree current.
  5. The browser paints clicked 1.

For lists, the same machinery eventually reaches reconcileChildrenArray: match by key, reuse fibers, mark placements and deletions.

What this means when you write React

Habit Why the runtime cares
Stable keys Correct matching; fewer remounts and misplaced state
Don’t swap component types at the same position Avoids tearing down a whole subtree ({cond ? <A /> : <B />})
Keep renders cheap The render phase may run more than once under concurrent features
Keep state close to where it is used Avoids scheduling broad updates from high in the tree
React.memo when profiling justifies it Can let React skip rendering a child when props are unchanged
useMemo / useCallback selectively Stabilize props for a memoized child; they do not skip renders by themselves
// Remounts on every toggle — local state inside Input is lost
{isSearch ? <SearchInput /> : <FilterInput />}
 
// Same component type at the same position — state can be preserved
<Input mode={isSearch ? "search" : "filter"} />

A mental model to keep

  1. JSX produces element trees — lightweight, disposable descriptions.
  2. Fiber is a linked tree of work — stateful, interruptible, paired current ↔ WIP.
  3. Reconciliation matches previous fibers to next elements with type + key heuristics.
  4. The render phase marks what should change and may pause or restart.
  5. The commit phase applies host mutations and layout work in one synchronous pass.

Once you see updates as “walk a linked tree, mark flags, then commit,” a lot of React’s behavior — keys, remounts, concurrent rendering, Profiler shapes — starts to feel inevitable rather than magical.

Further reading