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.
Top comments (0)