React.Fragment, the only child

nicolasamabile

Nicolas Amabile

Posted on April 21, 2019

React.Fragment, the only child

This is a short post about some issues I had while building a wizard component in ReactJS.

  • You can't reference a "falsy" child while using React.cloneElement.
  • React.Fragment returns a single child.

At the beginning my wizard instance looked something like this:

<Wizard>
  <Step1 />
  <Step2 />
  <Step3 />
  <Step4 />
  <Step5 />
</Wizard>
Enter fullscreen mode Exit fullscreen mode

Behind the scenes, the component will only render the current step.

render () {
  const { children } = this.props
  const { activeStep } = this.state
  const extraProps = {...} // Some extra info I need on each step.
  return (
    …
    {React.cloneElement(children[activeStep], extraProps)}
    …
  )
}
Enter fullscreen mode Exit fullscreen mode

Based on some business rules, I wanted to hide/show some steps, so my wizard instance will look something like this:

renderStep2 () {
  if (conditionForStep2) {
    return <Step2 />
  }
}
render () {
  return ( 
    <Wizard>
      <Step1 />
      {this.renderStep2()}
      <Step3 />
      {conditionForStep4 && <Step4 />}
      <Step5 />
    </Wizard>
  )
}
Enter fullscreen mode Exit fullscreen mode

Those expressions evaluate to undefined for Step2 and false for Step4, and any of those values can be used as a valid child when doing React.cloneElement(children[activeStep], extraProps) where activeStep is the index of Step2 or Step4, React will complain 😩 and also my index will be wrong.
React error. Element type is invalid

Instead of using children directly, I created a function that returns only the "truthy" steps:

const getChildren = children => children.filter(child => !!child)
And change my Wizard render function to something like this:
render () {
 const { children } = this.props
 const { activeStep } = this.state
 const filteredChildren = getChildren(children)
 return (
   …
   {React.cloneElement(filteredChildren[activeStep], extraProps)}
   …
 )
}
Enter fullscreen mode Exit fullscreen mode

The first problem solved 🎉

I got to the point where I wanted to group some steps in order to simplify my logic. Let's say for example that I need to use the same condition for rendering Step3, Step4 and Step5, so I grouped them into a React.Fragment.

renderMoreSteps () {
  if (condition) {
    return (
      <Fragment>
        <Step3 />
        <Step4 />
        <Step5 />
      </Fragment>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

And my Wizard instance:

<Wizard>
  <Step1 />
  <Step2 />
  {this.renderMoreSteps()}
</Wizard>
Enter fullscreen mode Exit fullscreen mode

The problem: Even though Fragment is not represented as DOM elements, it returns a single child instead of individual child components.
The solution: flatten children.

import { isFragment } from 'react-is'
const flattenChildren = children => {
  const result = []
  children.map(child => {
    if (isFragment(child)) {
      result.push(…flattenChildren(child.props.children))
    } else {
      result.push(child)
    }
  })
  return result
}
Enter fullscreen mode Exit fullscreen mode

Updated getChildren function:

const getChildren = children => flattenChildren(children).filter(child => !!child && !isEmpty(child))
Enter fullscreen mode Exit fullscreen mode

For simplicity, I used react-is, but the implementation is straight forward:

function isFragment (object) {
  return typeOf(object) === REACT_FRAGMENT_TYPE
}
const REACT_FRAGMENT_TYPE = hasSymbol
  ? Symbol.for('react.fragment')
  : 0xeacb;
const hasSymbol = typeof Symbol === 'function' && Symbol.for;
Enter fullscreen mode Exit fullscreen mode

I hope this helps!
All comments are welcomed.

💖 💪 🙅 🚩
nicolasamabile
Nicolas Amabile

Posted on April 21, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

React.Fragment, the only child
javascript React.Fragment, the only child

April 21, 2019