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:
- Reconciliation — figuring out what changed
- 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:
- Different types mean different trees. A
<div>becoming a<span>at the same position is treated as a replacement, not a clever morph. - Keys identify siblings. Among children of one parent,
keytells 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.
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:
child— first childsibling— next siblingreturn— parent
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:
- current — what is on screen
- work-in-progress (WIP) — the tree being prepared for the next commit
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 (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:
- bail out early when props and context did not meaningfully change
- call your function component (or class
render) - reconcile returned children against previous child fibers
- set flags (
Placement,Update,ChildDeletion, …) and bubblesubtreeFlags
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:
- before mutation — snapshot reads (for example
getSnapshotBeforeUpdate) - mutation — apply host inserts, updates, and deletes indicated by flags
- 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>
);
}- A click schedules an update on the
Counterfiber (a lane / priority). - React clones work into the WIP tree and runs
Counterwith the next state. - The
<button>fiber keeps its identity; the text child is marked for an update. - When the root finishes rendering, React commits the text mutation and makes the finished tree current.
- 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
- JSX produces element trees — lightweight, disposable descriptions.
- Fiber is a linked tree of work — stateful, interruptible, paired current ↔ WIP.
- Reconciliation matches previous fibers to next elements with type + key heuristics.
- The render phase marks what should change and may pause or restart.
- 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.