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.
This is actually a gotcha!
React won't create fibers for top level components without a key prop.
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.