How does React handle empty values(null/undfined/Booleans) internally?

We all know that Booleans, Null, and Undefined Are Ignored, examples like below render all same thing.

jsx
<div />
<div></div>
<div>{false}</div>
<div>{null}</div>
<div>{undefined}</div>
<div>{true}</div>
jsx
<div />
<div></div>
<div>{false}</div>
<div>{null}</div>
<div>{undefined}</div>
<div>{true}</div>

But how exactly are these values being handled internally by React? Let’s find it out.

These values are not components, so they only exist as children of some components, so let’s have a try at reconcileChildren().

In reconcileChildren(), mountChildFibers() is used for first render, and reconcileChildFibers() is for latter renders.

But actually these 2 functions are the same with one difference of whether to track side effects or not.

(source)

js
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
js
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

From ChildReconciler(), we can see that side effects means “deletion”.

ChildReconciler() consists a few closure functions under above flag, exporting the true reconcileChildFibers().

js
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes
): Fiber | null {
if (typeof newChild === "object" && newChild !== null) {
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:
if (enableLazyElements) {
const payload = newChild._payload;
const init = newChild._init;
// TODO: This function is supposed to be non-recursive.
return reconcileChildFibers(
returnFiber,
currentFirstChild,
init(payload),
lanes
);
}
}
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes
);
}
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes
);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
if (
(typeof newChild === "string" && newChild !== "") ||
typeof newChild === "number"
) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
"" + newChild,
lanes
)
);
}
// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
js
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes
): Fiber | null {
if (typeof newChild === "object" && newChild !== null) {
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:
if (enableLazyElements) {
const payload = newChild._payload;
const init = newChild._init;
// TODO: This function is supposed to be non-recursive.
return reconcileChildFibers(
returnFiber,
currentFirstChild,
init(payload),
lanes
);
}
}
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes
);
}
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes
);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
if (
(typeof newChild === "string" && newChild !== "") ||
typeof newChild === "number"
) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
"" + newChild,
lanes
)
);
}
// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}

It has 4 steps

  1. handle single element type based on $$typeof.
  2. handle array or iterators
  3. non-empty string and numbers
  4. the rest are treated as empty, leading to deletion of previous fiber if any.

So we can see that null, undefined and booleans are simply ignored when creating fiber.

What about the case when it is in an array, let’s look at reconcileChildrenArray().

reconcileChildrenArray() has some algorithm I’d like to cover later.

Looking at the code, we see 2 places where new fibers might be created.

js
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes
);
js
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes
);

In reconcileChildrenArray(), a new linked fiber list is constructed by looping through the array items.

If newFiber is null, it is simply ignored and not put on the fiber tree.

In updateSlot() and updateFromMap(), we find the similar pattern in which empty values are simply ignored and null is returned.

js
function updateSlot(
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
...
}
if (typeof newChild === 'object' && newChild !== null) {
...
}
return null;
}
js
function updateSlot(
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
...
}
if (typeof newChild === 'object' && newChild !== null) {
...
}
return null;
}

That’s it. We now know how empty values are handled in React - the are simply ignored.

One slight problem is that actually they affect the reconciling algorithm in reconcileChildrenArray(), which I’ll write a post about soon, stay tuned.

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: Easily understand Contravariance of function arguments in TypeScript

Next: How does 'key' work internally? List diffing in React