Sharing state between child components using React.Children

# React Tips# Snippets

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>
  )
}