The most obvious and common prop that developers work with within React is the children
prop. In the majority of cases, there is no need to understand how the children
prop looks like. But in some cases, we want to inspect the children
prop to maybe wrap each child in another element/component or to reorder or slice them. In those cases inspecting how the children
prop looks like becomes essential.
In this article, we’ll look at a React utility React.Children.toArray
which lets us prepare the children
prop for inspection and iteration, some of its shortcomings and how to overcome them — through a small open-source package, to keep our React code function the way it is deterministically supposed to behave, keeping performance intact. If you know the basics of React and have at least an idea about what the children
prop in React is, this article is for you.
While working with React, most of the time we do not touch the children
prop any more than using it in React components directly.
function Parent({ children }) {
return <div className="mt-10">{children}</div>;
}
But sometimes we have to iterate over the children
prop so that we can enhance or change the children without having the user of the components explicitly do it themselves. One common use case is to pass the iteration index-related information to child components of a parent like so:
import { Children, cloneElement } from "react";
function Breadcrumbs({ children }) {
const arrayChildren = Children.toArray(children);
return (
<ul
style={{
listStyle: "none",
display: "flex",
}}
>
{Children.map(arrayChildren, (child, index) => {
const isLast = index === arrayChildren.length - 1;
if (! isLast && ! child.props.link ) {
throw new Error(
`BreadcrumbItem child no. ${index + 1}
should be passed a 'link' prop`
)
}
return (
<>
{child.props.link ? (
<a
href={child.props.link}
style={{
display: "inline-block",
textDecoration: "none",
}}
>
<div style={{ marginRight: "5px" }}>
{cloneElement(child, {
isLast,
})}
</div>
</a>
) : (
<div style={{ marginRight: "5px" }}>
{cloneElement(child, {
isLast,
})}
</div>
)}
{!isLast && (
<div style={{ marginRight: "5px" }}>
>
</div>
)}
</>
);
})}
</ul>
);
}
function BreadcrumbItem({ isLast, children }) {
return (
<li
style={{
color: isLast ? "black" : "blue",
}}
>
{children}
</li>
);
}
export default function App() {
return (
<Breadcrumbs>
<BreadcrumbItem
link="https://goibibo.com/"
>
Goibibo
</BreadcrumbItem>
<BreadcrumbItem
link="https://goibibo.com/hotels/"
>
Hotels
</BreadcrumbItem>
<BreadcrumbItem>
A Fancy Hotel Name
</BreadcrumbItem>
</Breadcrumbs>
);
}
Here we’re doing the following:
- We are using the
React.Children.toArray
method to ensure that thechildren
prop is always an array. If we do not do that, doingchildren.length
might blow because thechildren
prop can be an object, an array, or even a function. Also, if we try to use the array.map
method onchildren
directly it might blow up. - In the parent
Breadcrumbs
component we are iterating over its children by using the utility methodReact.Children.map
. - Because we have access to
index
inside the iterator function (second argument of callback function ofReact.Children.map
) we are able to detect if the child is last-child or not. - If it is the last child we clone the element and pass in the
isLast
prop to it so that the child can style itself based on it. - If it is not the last child, we ensure that all those children which aren’t the last child have a
link
prop on them by throwing an error if they don’t. We clone the element as we did in step 4. and pass theisLast
prop as we did before, but we also additionally wrap this cloned element in an anchor tag.
The user of Breadcrumbs
and BreadcrumbItem
doesn’t have to worry about which children should have links and how they should be styled. Inside the Breadcrumbs
component, it automatically gets handled.
This pattern of implicitly passing in props and/or having state
in the parent and passing the state and state changers down to the children as props is called the compound component pattern. You might be familiar with this pattern from React Router’s Switch
component, which takes Route
components as its children:
// example from react router docs
// https://reactrouter.com/web/api/Switch
import { Route, Switch } from "react-router";
let routes = (
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/:user">
<User />
</Route>
<Route>
<NoMatch />
</Route>
</Switch>
);
Now that we have established that there are needs where we have to iterate over children
prop sometimes, and having used two of the children utility methods React.Children.map
and React.Children.toArray
, let’s refresh our memory about one of them: React.Children.toArray
.
React.Children.toArray
Let’s start by seeing with an example what this method does and where it might be useful.
import { Children } from 'react'
function Debugger({children}) {
// let’s log some things
console.log(children);
console.log(
Children.toArray(children)
)
return children;
}
const fruits = [
{name: "apple", id: 1},
{name: "orange", id: 2},
{name: "mango", id: 3}
]
export default function App() {
return (
<Debugger>
<a
href="https://css-tricks.com/"
style={{padding: '0 10px'}}
>
CSS Tricks
</a>
<a
href="https://smashingmagazine.com/"
style={{padding: '0 10px'}}
>
Smashing Magazine
</a>
{
fruits.map(fruit => {
return (
<div key={fruit.id} style={{margin: '10px'}}>
{fruit.name}
</div>
)
})
}
</Debugger>
)
}
We have a Debugger
component, which does nothing much in terms of rendering — it just returns children
as is. But it does log two values: children
and React.Children.toArray(children)
.
If you open up the console, you’d be able to see the difference.
- The first statement which logs
children
prop, shows the following as its value’s data structure:
[
Object1, ----> first anchor tag
Object2, ----> second anchor tag
[
Object3, ----> first fruit
Object4, ----> second fruit
Object5] ----> third fruit
]
]
- The second statement which logs
React.Children.toArray(children)
logs:
[
Object1, ----> first anchor tag
Object2, ----> second anchor tag
Object3, ----> first fruit
Object4, ----> second fruit
Object5, ----> third fruit
]
Let’s read the method’s documentation in React docs to make sense of what is happening.
React.Children.toArray
returns thechildren
opaque data structure as a flat array with keys assigned to each child. Useful if you want to manipulate collections of children in your render methods, especially if you want to reorder or slicechildren
before passing it down.
Let’s break that down:
- Returns the
children
opaque data structure as a flat array. - With keys assigned to each child.
The first point says that that children
(which is an opaque data structure, meaning it can be an object, array, or a function, as described earlier) is converted to a flat array. Just like we saw in the example above. Additionally, this GitHub issue comment also explains its behavior:
It (
React.Children.toArray
) does not pull children out of elements and flatten them, that wouldn’t really make any sense. It flattens nested arrays and objects, i.e. so that[['a', 'b'],['c', ['d']]]
becomes something similar to['a', 'b', 'c', 'd']
.
React.Children.toArray(
[
["a", "b"],
["c", ["d"]]
]
).length === 4;
Let’s see what the second point (‘With keys assigned to each child.’) says, by expanding one child each from the previous logs of the example.
Expanded Child From console.log(children)
{
$typeof: Symbol(react.element),
key: null,
props: {
href: "https://smashingmagazine.com",
children: "Smashing Magazine",
style: {padding: "0 10px"}
},
ref: null,
type: "a",
// … other properties
}
Expanded Child From console.log(React.Children.toArray(children))
{
$typeof: Symbol(react.element),
key: ".0",
props: {
href: "https://smashingmagazine.com",
children: "Smashing Magazine",
style: {padding: "0 10px"}
},
ref: null,
type: "a",
// … other properties
}
As you can see, besides flattening the children
prop into a flat array, it also adds unique keys to each of its children. From the React docs:
React.Children.toArray()
changes keys to preserve the semantics of nested arrays when flattening lists of children. That is,toArray
prefixes each key in the returned array so that each element’s key is scoped to the input array containing it.
Because the .toArray
method might change the order and place of children
, it has to make sure that it maintains unique keys for each of them for reconciliation and rendering optimization.
Let’s give a little bit more attention to so that each element’s key is scoped to the input array containing it.
, by looking at the keys of each element of the second array (corresponding to console.log(React.Children.toArray(children))
).
import { Children } from 'react'
function Debugger({children}) {
// let’s log some things
console.log(children);
console.log(
Children.map(Children.toArray(children), child => {
return child.key
}).join('n')
)
return children;
}
const fruits = [
{name: "apple", id: 1},
{name: "orange", id: 2},
{name: "mango", id: 3}
]
export default function App() {
return (
<Debugger>
<a
href="https://css-tricks.com/"
style={{padding: '0 10px'}}
>
CSS Tricks
</a>
<a
href="https://smashingmagazine.com/"
style={{padding: '0 10px'}}
>
Smashing Magazine
</a>
{
fruits.map(fruit => {
return (
<div key={fruit.id} style={{margin: '10px'}}>
{fruit.name}
</div>
)
})
}
</Debugger>
)
}
.0 ----> first link
.1 ----> second link
.2:0 ----> first fruit
.2:1 ----> second fruit
.2:2 ----> third fruit
As you can see that the fruits, which were originally a nested array inside the original children
array, have keys that are prefixed with .2
. The .2
corresponds to the fact that they were a part of an array. The suffix, namely :0
,:1
, :2
are corresponding to the React elements’ (fruits) default keys. By default, React uses the index as the key, if no key is specified for the elements of a list.
So suppose you had three level of nesting inside children
array, like so:
import { Children } from 'react'
function Debugger({children}) {
const retVal = Children.toArray(children)
console.log(
Children.map(retVal, child => {
return child.key
}).join('n')
)
return retVal
}
export default function App() {
const arrayOfReactElements = [
<div key="1">First</div>,
[
<div key="2">Second</div>,
[
<div key="3">Third</div>
]
]
];
return (
<Debugger>
{arrayOfReactElements}
</Debugger>
)
}
The keys will look like
.$1
.1:$2
.1:1:$3
The $1
, $2
, $3
suffixes are because of the original keys put on the React elements in an array, otherwise React complains of lack of keys ? .
From whatever we’ve read so far we can come to two use cases for React.Children.toArray
.
- If there’s an absolute need that
children
should always be an array, you can useReact.Children.toArray(children)
instead. It’ll work perfectly even whenchildren
is an object or a function too. - If you have to sort, filter, or slice
children
prop you can rely onReact.Children.toArray
to always preserve unique keys of all the children.
There’s a problem with React.Children.toArray
?. Let’s look at this piece of code to understand what the problem is:
import { Children } from 'react'
function List({children}) {
return (
<ul>
{
Children.toArray(
children
).map((child, index) => {
return (
<li
key={child.key}
>
{child}
</li>
)
})
}
</ul>
)
}
export default function App() {
return (
<List>
<a
href="https://css-tricks.com"
style={{padding: '0 10px'}}
>
Google
</a>
<>
<a
href="https://smashingmagazine.com"
style={{padding: '0 10px'}}
>
Smashing Magazine
</a>
<a
href="https://arihantverma.com"
style={{padding: '0 10px'}}
>
{"Arihant’s Website"}
</a>
</>
</List>
)
}
If you see what gets rendered for the children of the fragment, you’ll see that both of the links get rendered inside one li
tag! ?
This is because React.Children.toArray
doesn’t traverse into fragments. So what can we do about it? Fortunately, nothing ? . We already have an open-sourced package called react-keyed-flatten-children
. It’s a small function that does its magic.
Let’s see what it does. In pseudo-code (these points are linked in the actual code below), it does this:
- It is a function that takes
children
as its only necessary argument. - Iterates over
React.Children.toArray(children)
and gathers children in an accumulator array. - While iterating, if a child node is a string or a number, it pushes the value as is in the accumulator array.
- If the child node is a valid React element, it clones it, gives it the appropriate key, and pushes it to the accumulator array.
- If the child node is a fragment, then the function calls itself with fragment’s children as its argument (this is how it traverses through a fragment) and pushes the result of calling itself in the accumulator array.
- While doing all this it keeps the track of the depth of traversal (of fragments), so that the children inside fragments would have correct keys, the same way as keys work with nested arrays, as we saw earlier above.
import {
Children,
isValidElement,
cloneElement
} from "react";
import { isFragment } from "react-is";
import type {
ReactNode,
ReactChild,
} from 'react'
/*************** 1. ***************/
export default function flattenChildren(
// only needed argument
children: ReactNode,
// only used for debugging
depth: number = 0,
// is not required, start with default = []
keys: (string | number)[] = []
): ReactChild[] {
/*************** 2. ***************/
return Children.toArray(children).reduce(
(acc: ReactChild[], node, nodeIndex) => {
if (isFragment(node)) {
/*************** 5. ***************/
acc.push.apply(
acc,
flattenChildren(
node.props.children,
depth + 1,
/*************** 6. ***************/
keys.concat(node.key || nodeIndex)
)
);
} else {
/*************** 4. ***************/
if (isValidElement(node)) {
acc.push(
cloneElement(node, {
/*************** 6. ***************/
key: keys.concat(String(node.key)).join('.')
})
);
} else if (
/*************** 3. ***************/
typeof node === "string"
|| typeof node === "number"
) {
acc.push(node);
}
}
return acc;
},
/*************** Acculumator Array ***************/
[]
);
}
Let’s retry our previous example to use this function and see for ourselves that it fixes our problem.
import flattenChildren from 'react-keyed-flatten-children'
import { Fragment } from 'react'
function List({children}) {
return (
<ul>
{
flattenChildren(
children
).map((child, index) => {
return <li key={child.key}>{child}</li>
})
}
</ul>
)
}
export default function App() {
return (
<List>
<a
href="https://css-tricks.com"
style={{padding: '0 10px'}}
>
Google
</a>
<Fragment>
<a
href="https://smashingmagazine.com"
style={{padding: '0 10px'}}>
Smashing Magazine
</a>
<a
href="https://arihantverma.com"
style={{padding: '0 10px'}}
>
{"Arihant’s Website"}
</a>
</Fragment>
</List>
)
}
Woooheeee! It works.
As an add-on, if you are new to testing — like I am at the point of this writing — you might be interested in 7 tests written for this utility function. It’ll be fun to read the tests to deduce the functionality of the function.
The Long Term Problem With Children
Utilities
“
React.Children
is a leaky abstraction, and is in maintenance mode.”
The problem with using Children
methods to change children
behavior is that they only work for one level of nesting of components. If we wrap one of our children
in another component, we lose composability. Let’s see what I mean by that, by picking up the first example that we saw — the breadcrumbs.
import { Children, cloneElement } from "react";
function Breadcrumbs({ children }) {
return (
<ul
style={{
listStyle: "none",
display: "flex",
}}
>
{Children.map(children, (child, index) => {
const isLast = index === children.length - 1;
// if (! isLast && ! child.props.link ) {
// throw new Error(`
// BreadcrumbItem child no.
// ${index + 1} should be passed a 'link' prop`
// )
// }
return (
<>
{child.props.link ? (
<a
href={child.props.link}
style={{
display: "inline-block",
textDecoration: "none",
}}
>
<div style={{ marginRight: "5px" }}>
{cloneElement(child, {
isLast,
})}
</div>
</a>
) : (
<div style={{ marginRight: "5px" }}>
{cloneElement(child, {
isLast,
})}
</div>
)}
{!isLast && (
<div style={{ marginRight: "5px" }}>></div>
)}
</>
);
})}
</ul>
);
}
function BreadcrumbItem({ isLast, children }) {
return (
<li
style={{
color: isLast ? "black" : "blue",
}}
>
{children}
</li>
);
}
const BreadcrumbItemCreator = () =>
<BreadcrumbItem
link="https://smashingmagazine.com"
>
Smashing Magazine
</BreadcrumbItem>
export default function App() {
return (
<Breadcrumbs>
<BreadcrumbItem
link="https://goibibo.com/"
>
Goibibo
</BreadcrumbItem>
<BreadcrumbItem
link="https://goibibo.com/hotels/"
>
Goibibo Hotels
</BreadcrumbItem>
<BreadcrumbItemCreator />
<BreadcrumbItem>
A Fancy Hotel Name
</BreadcrumbItem>
</Breadcrumbs>
);
}
Although our new component <BreadcrumbItemCreator />
rendered, our Breadcrumb
component doesn’t have any way to extract out the link
prop from it, because of which, it doesn’t render as link.
To fix this problem React team had come with — now defunct — experimental API called react-call-return.
Ryan Florence’s Video explains this problem in detail, and how react-call-return
fixed it. Since the package was never published in any version of React, there are plans to take inspiration from it and make something production-ready.
Conclusion
To conclude, we learned about:
- The
React.Children
utility methods. We saw two of them:React.Children.map
to see how to use it to make compound components, andReact.Children.toArray
in depth. - We saw how
React.Children.toArray
converts opaquechildren
prop — which could be either object, array or function — into a flat array, so that one could operate over it in required manner — sort, filter, splice, etc… - We learned that
React.Children.toArray
doesn’t traverse through React Fragments. - We learned about an open-source package called
react-keyed-flatten-children
and understood how it solves the problem. - We saw that
Children
utilities are in maintenance mode because they do not compose well.
You might also be interested in reading how to use other Children
methods to do everything you can do with children
in Max Stoiber’s blog post React Children Deep Dive.
Resources
- Compound components with react hooks
- React.Children.toArray array flattening github issue explanation
- React reconciliation: Recursing on children
React.Children.toArray
doesn’t traverse into fragmentsreact-keyed-flatten-children
react-keyed-flatten-children
tests- react-call-return
- Ryan Florence’s Video explaining react-call-return
- React team’s plan to replace
Children
utilities with something more composable - Max Stoiber’s
React Children
Deep Dive React.Children
is a leaky abstraction, and is in maintenance mode
Source: Smashingmagazine.com