A good[ish] website
Web development blog, loads of UI and JavaScript topics
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.
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:
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:
React.Children
has five methods:
There’s a another type of solution to the current index too 👇
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:
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 componentschild.type.prototype instanceof Foo
: class componentschild.props.mdxType === 'Foo'
: MDX, apparentlyI 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.
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
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:
I originally picked this technique up from Michael Paravano’s blog.
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.