In this article we discuss and learn about the use of repetition on React children
and the ways to do it. We will focus on one of the useful methods, React.Children.toArray
, which React gives us, which helps to repeat about the children
in a way that ensures performance and determinism.
The most obvious and common object that developers in React work with is the children
brace. In most cases, you do not have to understand how children
stut lyk. But in some cases we want the children
suggests that each child be wrapped in a different element / component, or to rearrange or cut it. In these cases, inspect how the children
prop seems to be becoming essential.
In this article we look at a React utility React.Children.toArray
with which we children
record for inspection and repetition, some of the shortcomings and how to overcome them – through a small open-source package, to maintain our React code function as it is deterministically supposed to act, and performance maintained To keep. If you know the basics of React and at least have an idea of what children
plug in React, this article is for you.
While working with React, we mostly do not touch the children
more than using it directly in React components.
function Parent({ children }) {
return <div className="mt-10">{children}</div>;
}
But sometimes we have to repeat about the children
plug so that we can improve or change the children without the user of the components doing so explicitly himself. One common use is to transfer the iteration index-related information to the parent components of a parent, as follows:
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 do the following:
- We use the
React.Children.toArray
method to ensure that thechildren
plug is always an array. If we do not do it, dochildren.length
can blow because thechildren
plug can be an object, an array or even a function. If we also try to use the array.map
method onchildren
directly it can inflate. - In the parent
Breadcrumbs
component that we repeat about its children using the utility methodReact.Children.map
. - Because we have access to
index
within the iterator function (second argument of callback function ofReact.Children.map
) we can determine whether the child is the last child or not. - If this is the last child, we clone the element and give the
isLast
support it so that the child can style himself on it. - If this is not the last child, we ensure that all children who are not the last child, a
link
support them by making a mistake if they do not. We clone the element as in step 4. and pass theisLast
plug as we did before, but we also turn this cloned element into an anchor plate as well.
The user of Breadcrumbs
and BreadcrumbItem
no need to worry about which kids should have links and how they should be designed. Within the Breadcrumbs
component, it is handled automatically.
This pattern of implicit bring in and / or have props state
in the parent and the transfer of the state and state exchangers to the children as props becomes the composite component pattern. You may know this pattern from React Router 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="https://smashingmagazine.com/">
<Home />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/:user">
<User />
</Route>
<Route>
<NoMatch />
</Route>
</Switch>
);
Now that we have determined that there are needs that we need to repeat children
sometimes, and after using two of the children’s utilities React.Children.map
and React.Children.toArray
, let’s refresh our memory of one of them: React.Children.toArray
.
React.Children.toArray
Let’s start with an example of what this method does and where it can 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 has nothing to do with version – it just comes back children
just like that. But it does record two values: children
and React.Children.toArray(children)
.
When you open the console, you can see the difference.
- The first statement to record
children
plug, shows the following as the data structure of its value:
[
Object1, ----> first anchor tag
Object2, ----> second anchor tag
[
Object3, ----> first fruit
Object4, ----> second fruit
Object5] ----> third fruit
]
]
- The second statement that notes
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 documentation of the method in React documents to make sense of what is happening.
React.Children.toArray
give thechildren
opaque data structure as a flat array with keys assigned to each child. Useful if you want to manipulate children’s collections in your rendering methods, especially if you want to rearrange or cutchildren
before it is transmitted.
Let’s break it down:
- Show the
children
opaque data structure as a flat array. - With keys assigned to each child.
The first point says that it children
(which is an opaque data structure, meaning that it can be an object, array, or function, as described earlier) is converted to a flat array. Just as we saw in the example above. Furthermore, this GitHub Note also explain its behavior:
This (
React.Children.toArray
) do not pull children out of elements and flatten them, it would not really make sense. It flattens nested arrays and objects, i.e. so[['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 extending one child each from the previous logs of the example.
Extended child of 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
}
Extended child of 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, except for the children
prop in a flat arrangement, it also adds unique keys to each of its children. From the React documents:
React.Children.toArray()
change keys to maintain the semantics of nested arrays when posting lists of children. In other words,toArray
prefixes for each key in the returned array so that the key of each element to the input sequence it contains is reached.
Because the .toArray
method can determine the order and location of children
, it should make sure that it contains unique keys for each reconciliation and optimization.
Let’s pay a little more attention 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, the fruit, which was originally a nested series in the original children
array, has prefixed keys .2
. The .2
correspond to the fact that they were part of a settlement. The suffix, namely :0
,:1
, :2
corresponds to the standard keys of the React elements (fruit). 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 levels of nest inside children
array, as follows:
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
.$1
.1:$2
.1:1:$3
The $1
, $2
, $3
suffixes are due to the original keys placed on the React elements in an array, otherwise React complains about a lack of keys ????.
From what we have read so far, we can come to two use cases React.Children.toArray
.
-
If it is an absolute need
children
must always be an array, which you can useReact.Children.toArray(children)
instead. It will work perfectly even whenchildren
is also an object or a function. -
If you need to sort, filter or cut
children
prop you can rely onReact.Children.toArray
to always keep the unique keys of all the children.
There is 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 is shown to the children of the snippet, you will see that both the links are displayed within one li
tag! ????
This is because React.Children.toArray
does not walk in fragments. So, what can we do about it? Luckily nothing ????. We already have an open package called react-keyed-flatten-children
. It’s a small function that performs its magic.
Let’s see what it does. In pseudocode (these points are linked to the actual code below), it does this:
- This is a feature that takes
children
as his only essential argument. - Iterate past
React.Children.toArray(children)
and collect children in an accumulator array. - While repeating, if a child node is a string or a number, it pushes the value as in the accumulator range.
- If the child node is a valid React element, clone it, give it the appropriate key, and push it to the battery pack.
- If the child node is a fragment, the function calls itself with fragments’ children as an argument (this is how it is walk through a fragment) and press the result to call itself in the accumulator range.
- While doing all this, it keeps track of the depth of intersection (of fragments) so that the children inside fragments have the right keys, in the same way that 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 try our previous example again to use this feature and see for ourselves that it solves 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, you may be interested in the latest testing-as I am currently writing this- 7 tests written for this utility. It would be nice to read the tests to deduce the function of the function.
The long-term problem with Children
Utilities
“
React.Children
is a leaky abstraction and is in maintenance mode. “
The problem with usage Children
methods to change children
behavior is that it only works for one level of components’ nest. If we wrap one of ours children
in another component we lose the compatibility. Let’s see what I mean by that, by taking the first example 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 />
delivered, we Breadcrumb
component has no way of link
as a result, it is not displayed as a link.
To resolve this issue, the React team received an experimental API, which has now been discontinued respond-call-return.
Ryan Florence’s video explain 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 draw inspiration from it and get something ready for production.
Closure
In conclusion, we learned about:
- The
React.Children
utility methods. We saw two of them:React.Children.map
to see how you can use it to make composite components, andReact.Children.toArray
in depth. - We’ve seen
React.Children.toArray
change opaquechildren
plug – which can be an object, array or function – in a flat array, so that one can work it in the required way – sort, filter, split, etc … - We learned it
React.Children.toArray
does not run through React Fragments. - We learned about an open-source package called
react-keyed-flatten-children
and understand how it solves the problem. - We saw it
Children
utilities are in maintenance mode because they do not put together well.
You may also be interested in reading how you can use others Children
methods to do everything you can do children
in Max Stoiber’s blog post Respond children deep dive.
Resources
(ks, vf, yk, il)