Fixing Children.toArray's thorny edges

22nd Nov 2019

Children.toArray with Fragments

The most common prop you’ll work with in React is children. In the vast majority of cases there’s no need to care what this prop contains, but there are some use cases where you’ll want to wrap each provided child or introspect the collection. There’s a built-in utility called Children.toArray enables you to do this.

But it has some gotchas: it won’t recurse into React Fragments, and if you call Children.toArray on those fragments it can be difficult to guarantee keys stay unique and constant, which is important for efficient re-rendering.

For example, take this component which wraps each child in a list item:

const ToolbarList = ({ children }) => (
  <ul role="menu" className="toolbar">
    {
      React.Children.toArray(children).map(child => (
        <li>{child}</li>
      ))
    }
  </ul>
)

So if we render:

<ToolbarList>
  <a href="/home">Home</a>
  <a href="/popular">Popular Posts</a>
  <a href="/profile">Profile</a>
  <a href="/profile">Profile</a>
</ToolbarList>

The result will be:

<ul role="menu" className="toolbar">
  <li>
    <a href="/home">Home</a>
  </li>
  <li>
    <a href="/popular">Popular Posts</a>
  </li>
  <li>
    <a href="/profile">Profile</a>
  </li>
</ul>

This allows us to render a list of links into a toolbar menu, but we could also pass it buttons, or special Link components without changing ToolbarList. Great! But what if we want to do more?

Familiarity and flexibility

Many popular libraries like the immensely popular react-router use this kind of flexible API to express app behaviour in React’s familiar, declarative style. Naturally, users will eventually extend their use of your API along with their familiarity of this style and hit a roadblock — your API does not support recursing into Fragments.

Enter grrowl/react-keyed-flatten-children: whether you’re introspecting props.children to provide functionality like react-router, or simply cloning and wrapping children elements, as in our example above, this provides a drop-in replacement to Children.toArray which will robustly provide a “flat” array of keyed children which conforms to familiar and predictable React behaviour.

Revisit our example

Let’s switch in our react-keyed-flatten-children and look at the affordances it provides:

import flattenChildren from 'react-keyed-flatten-children'

const ToolbarList = ({ children }) => (
  <ul role="menu" className="toolbar">
    {
      flattenChildren(children).map(child => (
        <li>{child}</li>
      ))
    }
  </ul>
)

And we’ll wrap the “user links” in a fragment which renders conditionally:

<ToolbarList>
  <a href="/home">Home</a>
  <a href="/popular">Popular Posts</a>
  {
    isLoggedIn && (
      <>
        <a href="/profile">Profile</a>
        <a href="/settings">Settings</a>
      </>
    )
  }
</ToolbarList>

The result will be:

<ul role="menu" className="toolbar">
  <li>
    <a href="/home">Home</a>
  </li>
  <li>
    <a href="/popular">Popular Posts</a>
  </li>
  <li>
    <a href="/profile">Profile</a>
  </li>
  <li>
    <a href="/settings">Settings</a>
  </li>
</ul>

The result is users of your library or component can use built-in React features without tripping up, which means a more comfortable experience for your users and less issues raised on your repo out of confusion.

View the codesandbox example to get a better idea of the functionality of this module, or check out the code on github:

✨✨✨

grrowl/react-keyed-flatten-children

Flattens React children and fragments to an array with predictable and stable keys

✨✨✨

Thanks!