Addressing 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:

import React from 'react'

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>
<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?

Using fragments

We’ll wrap the “user links” in a fragment which renders conditionally if you’re logged in. Looking at the code it’s clear what the result should be (both profile and settings are only rendered if isLoggedIn is true):

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

But rendering using React.Children.toArray yields:

<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>
    <a href="/settings">Settings</a>
  </li>
</ul>

Two links in the same item! This is not what we intended.

Familiarity and flexibility

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

Enter react-keyed-flatten-children: whether you’re introspecting props.children to provide functionality like react-router, or simply mapping and wrapping children, as in our example above, this module 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 react-keyed-flatten-children and see how it affects how the behaviour of our app:

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

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

For our conditionally rendered fragment example, 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>

Nice!

The end result is users of your library or component can rely on built-in React features without tripping up, which means a more comfortable experience 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!