Skip to main content

How Reconciliation works

While rendering, reconciliation is the progress of computing and affecting the first child of the currently rendered fiber. This will go recursively until it reaches the bottom of the tree, then completeWork will kick and start climbing back.

The first ever call to the reconciliation in the lifetime of your app is when rendering the HostRoot and we'd need to transition to the next child which is what you gave to root.render().

Later on, reconciliation will follow the same mechanics and will always compute the next children.

How reconcileChildren works

reconcileChildren will be the entry point to this process:

export function reconcileChildren(
current: Fiber | null, // the current painted fiber
workInProgress: Fiber, // its alternate
nextChildren: any, // the next children
renderLanes: Lanes, // the render lanes
) {
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}

function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
) {
thenableIndexCounter = 0;
const firstChildFiber = reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
thenableState = null;
return firstChildFiber;
}

This function will call the reconcileChildFibers defined in the ReactChildFiber module.

We will omit talking about thenable for now for the clarity of this section.

Following up, reconcileChildFibersImpl will be called with the same arguments.

How reconcileChildFibersImpl works

This function will first decide on the next children to use: It will skip over a top level Fragment without a key property.

note

This is actually a gotcha!

React won't create fibers for top level components without a key prop.

tip

From the signature, we can deduce that the reconciliation goal actually is to translate a ReactNode to its equivalent Fiber.

function reconcileChildFibersImpl(
returnFiber: Fiber, // the parent fiber
currentFirstChild: Fiber | null, // the currently painted first child's fiber
newChild: any, // the next react node
lanes: Lanes, // the render lanes
): Fiber | null {

const isUnkeyedTopLevelFragment =
typeof newChild === 'object' && // a react element, not an array of them
newChild !== null && // typeof null is object 🙄
newChild.type === REACT_FRAGMENT_TYPE && // a Fragment
newChild.key === null; // key prop

if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}

if (typeof newChild === 'object' && newChild !== null) {
// treat objects (react elements, arrays ...)
}

if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
// treat text nodes
}

if (__DEV__) {
if (typeof newChild === 'function') {
warnOnFunctionType(returnFiber);
}
}
// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}

When the received newChild (nextChildren) is a non-null object, then there are two possibilities: either it is a single node, or a collection of them.

Here is how it distinguishes between them:

if (typeof newChild === 'object' && newChild !== null) {
// single child
switch (newChild.$$typeof) {

}

// collection of children
if (isArray(newChild)) {

}
if (getIteratorFn(newChild)) {

}

// async components
if (typeof newChild.then === 'function') {

}
throwOnInvalidObjectType(returnFiber, newChild);
}

How single child reconciliation works

When we have only one child with a valid $$typeof property, then the following cases are treated:

switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_LAZY_TYPE:
const payload = newChild._payload;
const init = newChild._init;
return reconcileChildFibers(
returnFiber,
currentFirstChild,
init(payload),
lanes,
);
}

The key functions used above are:

  • placeSingleChild
  • reconcileSingleChild
  • reconcileSinglePortal
  • recurse on lazy types: this recursion will land mostly in the thenables case and then enter reconciliation again.

How reconcileSingleElement works

This function continues to keep the same signature, but this time the newChild parameter is pretty well-known and is a ReactElement:

function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {

const key = element.key; // upcoming key
let child = currentFirstChild;

// loop over the previous tree to search for an element with the same key
while(child !== null) {
// compare key first
if (child.key === key) {
// after that, base logic on elementType
const elementType = element.type; // the new element type

} else {
deleteChild(returnFiber, child);
}

child = child.sibling;
}

if (element.type === REACT_FRAGMENT_TYPE) {
// ...
} else {
// ...
}

}

As you can see, reconcileSingleElement will first loop over the existing children and try to find the one with the same key.

If found, it will then test against the element.type, it has a special path for the Fragment type.

while (child !== null) {
if (child.key === key) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
// here, we are re-rendering a previous component
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
return existing;
}
// if not a fragment, then we fallthrough the deleteChild
} else {
// here, regular elements (not fragments)
if (
// the component stayed with same type
child.elementType === elementType ||
// special case for React.lazy
(typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
return existing;
}
}
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}

child = child.sibling;
}

So when found, React would deleteRemainingChildren then clone the existing fiber and attach it to its parent (returnFiber) then return it. Fragment doesn't have a ref, so the coerceRef part isn't there.

The deleteRemainingChildren will remove all fibers from the children array of the return fiber starting from the given one.

The process removal will add the deleted fiber to the deletions property of its parent:

// simplified
function deleteRemainingChildren(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
): null {
let childToDelete = currentFirstChild;
while (childToDelete !== null) {
deleteChild(returnFiber, childToDelete);
childToDelete = childToDelete.sibling;
}
return null;
}

function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
const deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber.deletions = [childToDelete];
returnFiber.flags |= ChildDeletion;
} else {
deletions.push(childToDelete);
}
}

After dealing with the previous tree and if we didn't match yet, (on mount, there is no current tree, so we jump here from the start), then React will create a new Fiber from the given React element and return it.

if (element.type === REACT_FRAGMENT_TYPE) {
const fiber = createFiberFromFragment(
element.props.children, // props
returnFiber.mode, // mode
lanes, // render lanes
element.key, // key
);
fiber.return = returnFiber;
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}

The fiber creation is detailed in the next section, we will see how it goes for every type.

coerceRef

How reconcileSinglePortal works

This section is not available yet. Please fill an issue.

While waiting for the newsletter, you can get notified when new content drops by following me on X.

How placeSingleChild works

The placeSingleChild function will mark the fiber of a Placement flag when it is constructed for the first time (no alternate).

if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags |= Placement | PlacementDEV;
}
return newFiber;

How reconcile lazy works

This section is not available yet. Please fill an issue.

While waiting for the newsletter, you can get notified when new content drops by following me on X.

How children array reconciliation works

This section is not available yet. Please fill an issue.

While waiting for the newsletter, you can get notified when new content drops by following me on X.

How reconcileChildrenArray works

This section is not available yet. Please fill an issue.

While waiting for the newsletter, you can get notified when new content drops by following me on X.

How reconcileChildrenIterator works

This section is not available yet. Please fill an issue.

While waiting for the newsletter, you can get notified when new content drops by following me on X.

How text nodes reconciliation works

This section is not available yet. Please fill an issue.

While waiting for the newsletter, you can get notified when new content drops by following me on X.