How does React traverse Fiber tree internally?

Fiber Tree Structure

As we’ve explained before, React keep a Fiber tree internally and keeps updating it. Each fiber node has 3 properties to form the structure.

  1. child, per name, its child fiber.
  2. sibling, per name, its sibling fiber
  3. return, you can think of it as (kinda) parent fiber.

There is no children, I guess React team wants to treat the tree traversal as simple as possible, so these 3 properties would work good enough.

We might have something like this.

How Fiber Tree is traversed

There is plenty of scenarios where the fiber tree needs to be traversed, for example this is how React traverse the fiber tree to mount passive effects (such as useEffect()) (source)

js
export function commitPassiveMountEffects(
root: FiberRoot,
finishedWork: Fiber
): void {
nextEffect = finishedWork;
commitPassiveMountEffects_begin(finishedWork, root);
}
function commitPassiveMountEffects_begin(subtreeRoot: Fiber, root: FiberRoot) {
while (nextEffect !== null) {
const fiber = nextEffect;
const firstChild = fiber.child;
if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && firstChild !== null) {
ensureCorrectReturnPointer(firstChild, fiber);
nextEffect = firstChild;
} else {
commitPassiveMountEffects_complete(subtreeRoot, root);
}
}
}
function commitPassiveMountEffects_complete(
subtreeRoot: Fiber,
root: FiberRoot
) {
while (nextEffect !== null) {
const fiber = nextEffect;
if ((fiber.flags & Passive) !== NoFlags) {
setCurrentDebugFiberInDEV(fiber);
try {
commitPassiveMountOnFiber(root, fiber);
} catch (error) {
reportUncaughtErrorInDEV(error);
captureCommitPhaseError(fiber, fiber.return, error);
}
resetCurrentDebugFiberInDEV();
}
if (fiber === subtreeRoot) {
nextEffect = null;
return;
}
const sibling = fiber.sibling;
if (sibling !== null) {
ensureCorrectReturnPointer(sibling, fiber.return);
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}
js
export function commitPassiveMountEffects(
root: FiberRoot,
finishedWork: Fiber
): void {
nextEffect = finishedWork;
commitPassiveMountEffects_begin(finishedWork, root);
}
function commitPassiveMountEffects_begin(subtreeRoot: Fiber, root: FiberRoot) {
while (nextEffect !== null) {
const fiber = nextEffect;
const firstChild = fiber.child;
if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && firstChild !== null) {
ensureCorrectReturnPointer(firstChild, fiber);
nextEffect = firstChild;
} else {
commitPassiveMountEffects_complete(subtreeRoot, root);
}
}
}
function commitPassiveMountEffects_complete(
subtreeRoot: Fiber,
root: FiberRoot
) {
while (nextEffect !== null) {
const fiber = nextEffect;
if ((fiber.flags & Passive) !== NoFlags) {
setCurrentDebugFiberInDEV(fiber);
try {
commitPassiveMountOnFiber(root, fiber);
} catch (error) {
reportUncaughtErrorInDEV(error);
captureCommitPhaseError(fiber, fiber.return, error);
}
resetCurrentDebugFiberInDEV();
}
if (fiber === subtreeRoot) {
nextEffect = null;
return;
}
const sibling = fiber.sibling;
if (sibling !== null) {
ensureCorrectReturnPointer(sibling, fiber.return);
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}

Because of what React does, each node is stepped on twice, begin() and complete(), you can think of it as the DOM event which has capture and bubble phase, begin() is the capture phase, and complete() is the bubble phase.

Parent fiber node will begin() first but complete() latter than children.

This approach is everywhere in React source code, with postfixes of begin and complete, we must get familiar with it before we dive in.

Coding Challenge

To enhance our understanding of the algorithm, I’ve put up a coding challenge. Enjoy!

Want to know more about how React works internally?
Check out my series - React Internals Deep Dive!

😳 Would you like to share my post to more people ?    

❮ Prev: How does React.memo() work internally?

Next: The lifecycle of effect hooks in React