Fundamentals
React Children

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, especially map and forEach. Below is a graph how React.Children is implemented with map. The forEach 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:

  1. Splitting the array returning from map
  2. pool is great. Because Children was mostly handled in render function so it will be called more frequently (hence a lot of recursion work). Setting a pool 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.