React Children
I didn't want to talk about
React.Children
at the beginning. First of all other than layout I don't use it a lot, on the other hand it is similar to how you would handle an array so it's easy to understand. But after checking source code, how they handle it is actually quite interesting, especiallymap
andforEach
. Below is a graph howReact.Children
is implemented withmap
. TheforEach
is similar but the difference is it doesn't return a new node.
THE graph:
//TODO
Confused? I know. I will discuss them based on each parts on this graph then you might have a better understanding.
Beginning
//TODO: This implementation is not correct in the latest version of React. Need to update. Removed after discussion (opens in a new tab)
function mapChildren(children, func, context) {
if (children == null) {
return children;
}
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, func, context);
return result;
}
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
let escapedPrefix = "";
if (prefix != null) {
escapedPrefix = escapeUserProvidedKey(prefix) + "/";
}
const traverseContext = getPooledTraverseContext(
array,
escapedPrefix,
func,
context
);
traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
releaseTraverseContext(traverseContext);
}
The biggest different between map
and forEach
is forEach
doesn't have return result
.
getPooledTraverseContext
will look for a target in pool
, and then releaseTraverseContext
will clean up the current items in context
and then put it back into pool
.
const POOL_SIZE = 10;
const traverseContextPool = [];
function getPooledTraverseContext() {
// args
if (traverseContextPool.length) {
const traverseContext = traverseContextPool.pop();
// set attrs
return traverseContext;
} else {
return {
/* attrs */
};
}
}
function releaseTraverseContext(traverseContext) {
// clear attrs
if (traverseContextPool.length < POOL_SIZE) {
traverseContextPool.push(traverseContext);
}
}
Does this mean pool
will only contains one value based on this code? It looks like it is looping on the action of pop
and push
. The answer is no. One of the attributes of React.Children.map
is it will keep working (a recursion process) if the return result is an array. One layer of recursion looks like:
function traverseAllChildren(children, callback, traverseContext) {
if (children == null) {
return 0;
}
return traverseAllChildrenImpl(children, "", callback, traverseContext);
}
function traverseAllChildrenImpl(
children,
nameSoFar,
callback,
traverseContext
) {
const type = typeof children;
if (type === "undefined" || type === "boolean") {
children = null;
}
let invokeCallback = false;
if (children === null) {
invokeCallback = true;
} else {
switch (type) {
case "string":
case "number":
invokeCallback = true;
break;
case "object":
switch (children.$$typeof) {
case REACT_ELEMENT_TYPE:
case REACT_PORTAL_TYPE:
invokeCallback = true;
}
}
}
if (invokeCallback) {
callback(
traverseContext,
children,
nameSoFar === ""
? SEPARATOR + getComponentKey(children, 0)
: nameSoFar
);
return 1;
}
let child;
let nextName;
let subtreeCount = 0; // Count of children found in the current subtree.
const nextNamePrefix =
nameSoFar === "" ? SEPARATOR : nameSoFar + SUBSEPARATOR;
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
child = children[i];
nextName = nextNamePrefix + getComponentKey(child, i);
subtreeCount += traverseAllChildrenImpl(
child,
nextName,
callback,
traverseContext
);
}
} else {
const iteratorFn = getIteratorFn(children);
if (typeof iteratorFn === "function") {
// iterator,similar to array
} else if (type === "object") {
// checking if the children's type is not correct
}
}
return subtreeCount;
}
For any loopable (children that haven't end) it will continue to use traverseAllChildrenImpl
until it reaches the point where there is only one node left, and then using a callback
-- mapSingleChildIntoContext
to handle it.
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
const { result, keyPrefix, func, context } = bookKeeping;
let mappedChild = func.call(context, child, bookKeeping.count++);
if (Array.isArray(mappedChild)) {
// attention
mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, (c) => c);
} else if (mappedChild != null) {
if (isValidElement(mappedChild)) {
mappedChild = cloneAndReplaceKey(
mappedChild,
keyPrefix +
(mappedChild.key &&
(!child || child.key !== mappedChild.key)
? escapeUserProvidedKey(mappedChild.key) + "/"
: "") +
childKey
);
}
result.push(mappedChild);
}
}
When you saw React.Children.map(children, callback)
, the second parameter is indeed mapSingleChildIntoContext
and returning the result after map
. The important part being if the result is an array, it will enter mapIntoWithKeyPrefixInternal
and then figure out the context
from the pool
.
Translator note: After discussion (opens in a new tab) in 2017, mapIntoWithKeyPrefixInternal
was removed (opens in a new tab)
// TODO: needs a better way to say this. This two paragraphs don't make sense to me.
But if the returned result is not an array + it is a valid ReactElement
, we will be reaching the end, then React will use cloneAndReplaceKey
to clone the element, replace the key
and then push it into the result array.
This implementation achieved two things in React:
- Splitting the array returning from
map
pool
is great. BecauseChildren
was mostly handled inrender
function so it will be called more frequently (hence a lot of recursion work). Setting apool
will reduce the creation of new objects and damages from garbage collection.
Not some magical code, but it is interesting to see how they implement this.