React.Fragment, the only child
Nicolas Amabile
Posted on April 21, 2019
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>
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)}
…
)
}
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>
)
}
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.
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)}
…
)
}
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>
)
}
}
And my Wizard instance:
<Wizard>
<Step1 />
<Step2 />
{this.renderMoreSteps()}
</Wizard>
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
}
Updated getChildren function:
const getChildren = children => flattenChildren(children).filter(child => !!child && !isEmpty(child))
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;
I hope this helps!
All comments are welcomed.
Posted on April 21, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.