Sharing state between child components using React.Children
Today I came across React.Children and I thought it was a great way to share state between child components without using the Context API.
In the following example, we have a parent component that has two child components. The parent component has a state and it passes the state and a setter function to the child components. The child components can then use the setter function to update the state of the parent component.
import React, { useState } from 'react'
const Parent = ({ children }) => {
const [state, setState] = useState('')
return <div>{children}</div>
}
One of the children is a setter component that has a button that when clicked, it sets the state of the parent component. This components accepts a setter function as a prop and uses it to set the state.
const SetterChild = ({ setState }) => {
return (
<div>
<button onClick={() => setState('1')}>Set state</button>
</div>
)
}
The second child is just displaying the state of the parent component.This component accepts the state as a prop and uses it to display the state.
const DisplayChild = ({ state }) => {
return (
<div>
<p>State: {state}</p>
</div>
)
}
And here is how we put together the parent and the children.
const App = () => {
return (
<div>
<Parent>
<SetterChild />
<DisplayChild />
</Parent>
</div>
)
}
Using React.Children
Now, we can use React.Children to pass the state and the setter function to the children. We can use the React.Children.map
function to map over the children and pass the state and the setter function to each child.
const Parent = ({ children }) => {
const [state, setState] = useState('')
return (
<div>
{React.Children.map(children, (child) => {
return React.cloneElement(child, { state, setState })
})}
</div>
)
}
Note that we are using React.cloneElement
to clone the child and pass the state and the setter function to it. That is
because we can't directly modify the props of a child component. We have to clone it first and then pass the props to it.
With this approach, we don't need to pass the state and the setter function to the children manually. I think it is a great way to avoid poluting the code with props.
What if one of the children is not a React component?
What happens if we want to add a div
or a p
tag as a child of the parent component? In that case, we are trying to clone and set props
on a element that it's type
is not a function.
const App = () => {
return (
<div>
<Parent>
<SetterChild />
<div>Some text</div>
<DisplayChild />
</Parent>
</div>
)
}
On thing we can do it to check the type
of the child
and if it is not a function, we can just return the child as is.
const Parent = ({ children }) => {
const [state, setState] = useState('')
return (
<div>
{React.Children.map(children, (child) => {
if (typeof child.type !== 'function') {
return child
}
return React.cloneElement(child, { state, setState })
})}
</div>
)
}
We can also restrict the use of DOM elements inside the parent component by checking the child.type
against string
.
const Parent = ({ children }) => {
const [state, setState] = useState('')
return (
<div>
{React.Children.map(children, (child) => {
if (typeof child.type === 'string') {
throw new Error(
`<${child.type} /> DOM elements are not allowed inside <Parent />`
)
}
return React.cloneElement(child, { state, setState })
})}
</div>
)
}
Problems with this approach
One big problem I can see with this is that it only works with one level of children. The following example will not work because
the Parent component is using React.Children.map
and passes props to the first level childs not all of them.
const App = () => {
return (
<div>
<Parent>
<SetterChild />
<div>
<DisplayChild />
</div>
</Parent>
</div>
)
}
Better off using Context API
I think this approach is great for simple cases where you don't need to share state between multiple levels of child components. If you need to share state between multiple levels of child components, I think it is better to use the Context API.
For example we can rewrite the above example using the Context API.
const MyStateContext = React.createContext()
const Parent = ({ children }) => {
const [state, setState] = useState('')
return (
<MyStateContext.Provider value={{ state, setState }}>
{children}
</MyStateContext.Provider>
)
}
const SetterChild = () => {
const { setState } = useContext(MyStateContext)
return (
<div>
<button onClick={() => setState('1')}>Set state</button>
</div>
)
}
const DisplayChild = () => {
const { state } = useContext(MyStateContext)
return (
<div>
<p>State: {state}</p>
</div>
)
}
const App = () => {
return (
<div>
<Parent>
<SetterChild />
<div>
<DisplayChild />
</div>
</Parent>
</div>
)
}