Question: If React already has a component tree, why does it need Fiber?
Short answer: A component tree describes what is nested inside what. A Fiber tree also records where React paused, what still needs work, and how to continue without rebuilding the call stack.
That distinction sounds small until a render is interrupted.
A normal recursive tree walk borrows the JavaScript stack. Once it enters a deep child, the browser cannot ask it to hand control back halfway through. Fiber moves the traversal state into heap objects. React can process one object, choose the next one, and yield between units of work when the renderer and scheduler allow it.
This article follows the actual shape used by React 19.2.7. It is not a substitute for the source. It is a map for reading the source without getting lost.
Start with the links, not the jargon
A simplified Fiber node looks like this:
const fiber = {
type: ProfileCard,
key: null,
pendingProps: { userId: 42 },
return: parentFiber,
child: firstChildFiber,
sibling: nextSiblingFiber,
memoizedProps: previousProps,
memoizedState: firstHook,
updateQueue: pendingUpdates,
flags: 0,
subtreeFlags: 0,
lanes: 0,
childLanes: 0,
alternate: otherVersionOfThisFiber,
};
The real constructor contains these fields and more in ReactFiber.js.
Three pointers turn the component hierarchy into a structure React can walk iteratively:
-
childpoints to the first child. -
siblingpoints to the next child of the same parent. -
returnpoints back to the parent.
Why call the parent link return? Because when a unit has no more child work, that is where control returns. The name describes the traversal, not the JSX relationship.
Consider this component:
function App() {
return (
<main>
<Header />
<Profile />
</main>
);
}
Its relevant Fiber links are closer to this:
App
|
child
v
main -> Header -> Profile
sibling sibling
Header.return = main
Profile.return = main
main.return = App
Header and Profile are visually siblings in JSX. In memory, only the first child hangs directly from main; later children form a sibling chain.
Try the walk yourself
Here is a deliberately small depth-first walker. It does not render anything, but its control flow mirrors the useful part of Fiber traversal.
function beginWork(node) {
console.log('begin', node.name);
return node.child;
}
function completeWork(node) {
console.log('complete', node.name);
}
function performUnitOfWork(node) {
const child = beginWork(node);
if (child) return child;
let cursor = node;
while (cursor) {
completeWork(cursor);
if (cursor.sibling) return cursor.sibling;
cursor = cursor.return;
}
return null;
}
let nextUnit = root;
while (nextUnit) {
nextUnit = performUnitOfWork(nextUnit);
}
The order is the point:
- Begin the current node.
- Descend into its first child if one exists.
- If there is no child, complete the node.
- Move sideways to a sibling.
- If there is no sibling, climb through
returnand complete ancestors until a sibling appears.
React's production code has branches for component types, Suspense, hydration, errors, profiling, and more. The spine is still recognizable. In React 19.2.7, performUnitOfWork calls beginWork. When no child is returned, it enters completion.
The call stack no longer owns the whole journey. The workInProgress pointer does.
What happens in begin and complete?
It is tempting to translate beginWork as "render this component" and completeWork as "put it on screen." The second half is wrong.
beginWork asks what this Fiber's children should be for the current render. For a function component, that includes calling the component through the Hooks machinery and reconciling the returned children. For a host component such as div, it reconciles the children prop.
The official source shows child reconciliation assigning workInProgress.child from either the mount or update path in ReactFiberBeginWork.js.
completeWork runs while the traversal climbs back up. Among other renderer-specific jobs, completion can prepare host instances and bubble information from descendants into their parent. It does not mean the browser DOM has already changed.
That matters because React's public mental model has three stages:
- Trigger a render.
- Render components and calculate the next UI.
- Commit the required changes.
React documents that separation in Render and Commit. The render phase may be restarted or abandoned. Visible mutation belongs to commit.
Where interruption becomes possible
Replace the final while loop in our toy walker with a deadline:
function workLoop(deadline) {
while (nextUnit && performance.now() < deadline) {
nextUnit = performUnitOfWork(nextUnit);
}
if (nextUnit) {
scheduleAnotherSlice(workLoop);
}
}
This is explanatory code, not React's scheduler. The useful idea is that nextUnit survives outside the JavaScript stack. A later callback can resume from the saved Fiber.
Current React has more than one work-loop path. One loop asks the Scheduler whether it should yield; another uses a time boundary. The exact policy can change. The stable concept is narrower: work is divided into resumable units, and concurrent rendering can yield between them.
This is also why "Fiber makes React asynchronous" is a poor explanation. A synchronous render still uses Fibers. Fiber is the work representation. Scheduling policy decides whether a particular render yields.
Reconciliation is an identity problem
Now ask a harder question:
When React sees new JSX, how does it know whether a Fiber represents the same thing as before?
The practical answer is type, position, and key.
function Results({ sorted }) {
const items = sorted ? sortByScore(data) : data;
return items.map((item) => (
<ResultRow key={item.id} item={item} />
));
}
The key is not decoration and it is not passed to ResultRow as a normal prop. It helps React match an old child Fiber with a new element among siblings.
If an item's position changes but its stable key remains, React can preserve the component's identity and state. If the key changes, React is allowed to treat it as a different component and remount it.
Index keys are therefore not universally forbidden. They are a claim: "the identity of each item is its current position." That claim is safe for a fixed, append-only list. It breaks when items reorder, disappear, or arrive in the middle.
The bug often looks supernatural because the DOM text is correct while local state appears to jump rows. The Fiber identity followed the index exactly as requested.
Why React keeps two versions
Each Fiber has an alternate field. During an update, React can keep the currently committed node and a work-in-progress version of that node.
React's own comment calls this a double-buffering pool. The alternate is created lazily, then reused. See createWorkInProgress.
That design gives React somewhere to write the next result without corrupting the tree currently associated with the screen. If a render cannot finish, the committed version still exists. When commit succeeds, the finished work becomes current.
Do not picture two permanent, independent application trees with every object duplicated forever. Alternates are paired and reused, and many fields may point to shared data until a render path needs to clone or replace it.
A debugging model that holds up
When a React bug feels random, I use four questions:
- Identity: Did type, position, or key make this a new Fiber?
- Phase: Did this code run while calculating or while committing?
- Traversal: Is React descending into child work, completing ancestors, or bailing out?
- Priority: Is this update in the work React chose to process now?
The first three now have a concrete shape. The fourth needs lanes, effects, and Hooks, which belong in Part 2.
Fiber is not "the virtual DOM object." It is React's mutable work record for one potential piece of UI. Once you see the links and the cursor, the name stops feeling mystical. It is a stack frame React can keep, revisit, clone, and schedule.
Next question: If the render phase only calculates, how do lanes decide what gets calculated first, and how do flags carry the answer into commit?


Top comments (1)
Very good and informative article @congar97 .. Keep it up 👏