Prop Drilling issue: Explore these options before using useContext()

React Context should not be the only option for resolving the issue of "prop drilling". You may want to consider these alternative approaches.



What is "Prop Drilling"

Prop drilling is passing props through intermediary components to reach the target component, which is the actual consumer. Consider the code below:

function App() {
  return <Parent />
}

function Parent() {
  const [count, setCount] = useState(0)
  return <Child count={count} />
}

function Child({ count }) {
  return <GrandChild count={count} />
}

function GrandChild({ count }) {
  return <div>Count is {count}</div>
}

what is prop drilling

In this example, count is passed through <Child/> before it reaches <GrandChild/>. The <Child/> component does not actually use the count prop but simply forwards it to the <GrandChild/>. This is "Prop Drilling".

Solutions

Option 1: Do nothing

Prop drilling is not always a problem. If we have only a few levels of components, it is simpler and more explicit to pass the props down. This also makes it clear what the inputs of each component are, as if they were arguments of a function.

Option 2: Component composition

We can directly pass the prop to the target component by making use of children prop.

function App() {
  return <Parent />
}

function Parent() {
  const [count, setCount] = useState(0)
  return (
    // We don't need to pass `count` prop to <Child/> anymore
    <Child>
      {/* Directly pass `count` to <GrandChild/> */}
      <GrandChild count={count} />
    </Child>
  )
}

// Make use of `children'
function Child({ children }) {
  return <>{children}</>
}

function GrandChild({ count }) {
  return <div>Count is {count}</div>
}

component composition

Note that in this case, <GrandChild/> does not accept any props from <Child/>.

But, what if we want to pass some props from <Child/> to <GrandChild/>? That's where option 3 comes in.

Option 3: Render prop

By using this pattern, <GrandChild/> can accept different props from multiple parent components.

function App() {
  return <Parent />
}

function Parent() {
  const [count, setCount] = useState(0)
  return (
    <Child
      // Render prop
      // <Child/> will pass `unit` to this function
      renderText={(unit) => {
        // - Directly pass `count` to <GrandChild/>
        // - Grab `unit` from the function arg and pass it to <GrandChild/> as well
        return <GrandChild count={count} unit={unit} />
      }}
    />
  )
}

function Child({ renderText }) {
  const [unit, setUnit] = useState('cat(s)')
  // Call the render prop by passing `unit` as an arg
  return <>{renderText(unit)}</>
}

function GrandChild({ count, unit }) {
  return (
    <div>
      Count is {count} {unit}
    </div>
  )
}

render prop

The Render Prop pattern may take some time to understand if you are not familiar with it.

Option 4: Context

Using Context should be the last resort as it changes the way we pass props from explicit to implicit. This approach can be useful in certain situations, but it is important to consider the downsides of using Context and to explore alternative options before resorting to it.

function App() {
  return (
    <Parent>
      <Child />
    </Parent>
  )
}

const CountContext = createContext(null)

function Parent({ children }) {
  const [count, setCount] = useState(0)
  return <CountContext.Provider value={count}>{children}</CountContext.Provider>
}

function Child() {
  // There is no prop explicitly passed to <GrandChild/>
  return <GrandChild />
}

function GrandChild() {
  // Implicitly receive `count` from Context instead of explicitly receive it from props
  const count = useContext(CountContext)
  return <div>Count is {count}</div>
}

context