clubmate.fi

A good[ish] website

Web development blog, loads of UI and JavaScript topics

React children and syntactic purity

Filed under: JavaScript— Tagged with: React

There might be more to React’s children prop than first meets the eye.

Children is one of the rare "magical" things in React, it’s a prop that just is there always.

Understanding React.Children through component index

Here’s a very basic example which maps a component onto an array, passes in the children and an index. Using the index, we make every other element a different color:

const ListItem = props => {
  return (
    <li style={{ color: props.index % 2 === 0 ? 'red' : 'blue' }}>
      {props.children} {props.index}
    </li>
  )
}

const List = props => {
  return <ul>{props.children}</ul>
}

const App = () => {
  return (
    <List>
      {['foo', 'bar', 'baz'].map((item, index) => (
        <ListItem key={item} index={index}>
          {item}
        </ListItem>
      ))}
    </List>
  )
}

It produces a list like so:

  • foo 0
  • bar 1
  • baz 2

What about doing the same thing but without the map construct? Like so:

const App = () => {
  return (
    <List>
      <Comp>foo</Comp>
      <Comp>bar</Comp>
      <Comp>baz</Comp>
    </List>
  )
}

This is a pattern is used a lot with compound components, it has a certain syntactical purity and it enables a freer arrangement of the components, it also resembles more HTML and in general is simpler.

But the downside is that we don’t have access to the index anymore, at least not the same way.

This is where React.Children() and React.cloneElement() come handy. We add the index prop behind the scenes:

const List = props => {
  const children = React.Children.map(props.children, (child, index) => {    return React.cloneElement(child, { index })  })
  return <ul>{children}</ul>
}

const CompoundList = () => {
  return (
    <List>
      <ListItem>foo</ListItem>
      <ListItem>bar</ListItem>
      <ListItem>baz</ListItem>
    </List>
  )
}

Should produce the same list as above:

  • foo 0
  • bar 1
  • baz 2

React.Children has five methods:

React.Children.map
This maps the children over like normal map does.
React.Children.forEach
This is like a map but does’t return anything.
React.Children.count
This returns the amount of children.
React.Children.only
Makes sure there is only one child, if more than one child, then it throws an error.
React.Children.toArray
Children isn’t actually an array, this makes it into an array.

There’s a another type of solution to the current index too 👇

Match the type of a children

You can, in some cases, match the type of children in the loop.

Say we have two types of component: Foo and Bar, both need to have their own index. The below example just prints out some divs:

import React from 'react'

const Foo = props => <div>Foo: {props.index}</div>
const Bar = props => <div>Bar: {props.index}</div>

const Wrap = props => {
  let fooCount = 0
  let barCount = 0
  const children = React.Children.map(props.children, child => {
    // Check for the type, this is troublesome tho, doesn’t work in MDX or    // if you wrap your component with styled-components for example. Read more    // below.    if (child.type === Foo) {
      return React.cloneElement(child, { index: fooCount++ })
    }

    return React.cloneElement(child, { index: barCount++ })
  })

  return children
}

const Matcher = () => {
  return (
    <Wrap>
      <Foo />
      <Foo />
      <Foo />
      <Bar />
      <Bar />
    </Wrap>
  )
}

export default Matcher

Both component types have their own indices:

Foo: 0
Foo: 1
Foo: 2
Bar: 0
Bar: 1

The iffy if statement

Note: that condition is a bit meh child.type === Foo, it doesn’t work on class components, and it doesn’t work if you’re using MDX, which makes me think there’s prob other cases where it also doesn’t work.

Here’s a slightly improved one:

// ❓ This might be dodgy ❓
if (
  child.props.mdxType === 'TabPanel' ||
  child.type.prototype instanceof TabPanel ||
  child.type === TabPanel
)
  • child.type === Foo: function components
  • child.type.prototype instanceof Foo: class components
  • child.props.mdxType === 'Foo': MDX, apparently

I haven’t really found a bullet proof way to check component type, there’s so many cases where it can fail. Feels like this is pushing what React is made for.

And if the component is wrapped, say, with styled-components it won’t work. The achilles heel of this system is the component wrapping, if you wrap, you have to do the same dance again in the wrapping component.

Hidden utility props

For recognizing element type we could also use "hidden" utility props.

This example is the same as above but, but we’re giving the components a __TYPE props via defaultProps:

import React from 'react'

const Foo = props => <div>Foo: {props.index}</div>

Foo.defaultProps = {
  __TYPE: 'Foo'}

const Bar = props => <div>Bar: {props.index}</div>

Bar.defaultProps = {
  __TYPE: 'Bar'}

const Wrap = props => {
  let fooCount = 0
  let barCount = 0
  const children = React.Children.map(props.children, child => {
    // Check the `__TYPE` from the props, this is a value you control
    if (child.props.__TYPE === 'Foo') {      return React.cloneElement(child, { index: fooCount++ })
    }

    return React.cloneElement(child, { index: barCount++ })
  })

  return children
}

const HiddenProps = props => {
  return (
    <Wrap>
      <Foo />
      <Foo />
      <Foo />
      <Bar />
      <Bar />
    </Wrap>
  )
}

export default HiddenProps

Type check the type

Nothing stops a dev for overriding the __TYPE, but it could be type checked, like so:

const checkType = componentType => (props, propName, componentName) => {
  if (props[propName] !== componentType) {
    return new Error(
      `'${propName}' in '${componentName}'\n\nPlease don’t modify '${propName}', it’s an internal prop\n`
    )
  }
}

const Foo = props => <div>Foo: {props.index}</div>

Foo.defaultProps = {
  __TYPE: 'Foo'
}

ToDo.propTypes = {
  children: PropTypes.oneOnType([PropTypes.node, PropTypes.array]),
  __TYPE: checkType('Foo')
}

Should work:

Foo: 0
Foo: 1
Foo: 2
Bar: 0
Bar: 1

I originally picked this technique up from Michael Paravano’s blog.

Conclusions

If you use React.Children.toArray you can do any kind of operation to the children, filter, find, some, all... This is a pretty powerful tool.

Hope this was helpful, thanks for blessing my analytics with your clicks!

Comments would go here, but the commenting system isn’t ready yet, sorry.

  • © 2022 Antti Hiljá
  • About
  • All rights reserved yadda yadda.
  • I can put just about anything here, no one reads the footer anyways.
  • I love u!