How To Avoid Prop Drilling in React Using Component Composition

Prop Drilling is the process by which you pass data from one part of the React Component tree to another by going through other parts that do not need the data but only help in passing it around.

Imagine someone living in Lagos, Nigeria placing an order on Amazon for a package. The package will have to go through many hands - it has to be flown by air to Nigeria, transported to the Lagos, moved from place to place until it gets into the hands of the buyer. At each stage, Amazon employs the services of people that do not 'care' about the product, they only help to 'pass' it to the person who really cared - the buyer.

Prop drilling is similar: You pass data (props) from Component1 to another Component2 - which doesn't need the data to render but only passes it to another Component3, which also doesn't need it and may pass it to another Component4. This may continue until the data gets to the ComponentNeedingProps.

Consider the code snippet below:

import React from "react";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <Component1 content="Who needs me?" />
    </div>
  );
}


function Component1({ content }) {
  return (
    <div>
      <h3>I am the first component</h3>;
      <Component2 content={content} />|
    </div>
  );
}

function Component2({ content }) {
  return (
    <div>
      <h3>I am the second component</h3>;
      <Component3 content={content} />
    </div>
  );
}

function Component3({ content }) {
  return (
    <div>
      <h3>I am the third component</h3>;
      <ComponentNeedingProps content={content} />
    </div>
  );
}

function ComponentNeedingProps({ content }) {
  return <h3>{content}</h3>;
}

The content prop is passed to Component1 in the root component, App. But Component1 does not need the prop to render, it only passes it to Component2, which passes it to Component3, which passes it to ComponentNeedingProps. It is this component that uses the content prop to render content in <h3>{content}</h3>.

Why is Prop Drilling a Problem?

Actually, prop drilling doesn't have to be a problem. If we are passing data only between 2 or 3 levels, we are fine. It will be easy to trace the flow of data. But imagine, we are drilling 5 levels...or 10 levels...or 15. Nah.

IAmFine.jpg

How To Solve The Problem?

Prop drilling is not a new problem in react (quite obviously), and there have been many solutions that let us pass data down to deeply nested Components.

One of which is Redux: You create a data store and connect any component to the store and voila, no matter where the component is positioned in the Component Tree it has access to the store.

React also has the concept of Context which lets you create something like a global data store and any Component in 'context' can have access to the data store.

But...

These techniques can be expensive. For example, using React Context API, when the Context value changes it triggers a re-render in all the children reading from the Context (except you work around it). In fact, the official React documentation has this to say:

If you only want to avoid passing some props through many levels, component composition is often a simpler solution than context

How to Avoid Prop Drilling Using Component Composition

Component Composition is when you compose different Components, usually simple, together to create more complex functionality. If you have ever written a React app, I bet that you have been composing components. Take for example:

function Button(props){
  return <button onClick={props.handleClick}>{props.text}</button>
}

const SignInButton = props => {
  return <Button text="SignIn" onClickHandler={props.handleClick} />
}

Here, by using composition we created another version of the Button component, SignInButton. You can read more on composition on React documentation page.

What is the actual problem we are trying to solve?

The actual problem is that we want ComponentNeedingProps to be rendered in Component3 but it needs data from the root component, App to do so. In other words, ComponentNeedingProps needs data from somewhere higher in the Component Tree (App) from where it is rendered (Component3).

The Solution? What if we can pass ComponentNeedingProps alongside its props from the root component, App, down to where it has to be rendered, that is, Component3?.

Yes, we can do that. We can pass down React Components as props too.

function App() {
  const content = "Who needs me?";
  const lastCompoent = <ComponentNeedingProps content={content} />;
  return (
    <div className="App">
      <Component1 lastComponent={lastComponent} />
    </div>
  );
}

See: The content prop is passed to ComponentNeedingProps right from the source of the prop, App component. Remember that ComponentNeedingProps has to be rendered in Component3...Now wait for the magic:

function Component1({ lastComponent}) {
  return (
    <div>
      <h3>I am the first component</h3>;
      <Component2 lastCompoent={lastComponent} />|
    </div>
  );
}

function Component2({ lastComponent}) {
  return (
    <div>
      <h3>I am the second component</h3>;
      <Component3 lastCompoent={lastComponent} />
    </div>
  );
}

function Component3({ lastComponent}) {
  return (
    <div>
      <h3>I am the third component</h3>
        {lastComponent}
    </div>
  );
}

function ComponentNeedingProps({ content }) {
  return <h3>{content}</h3>
}

Did you see it? We passed ComponentNeedingProps as a lastComponent prop, alongside the data it needs to render through the intermediate components until it got to where it should be rendered Component3 and then we rendered it:

...
{lastComponent}
...

Now, Component1 and Component2 do not have to 'know' about the props that ComponentNeedingProps needed, i.e the content prop.

We successfully avoided 'drilling' the props by Containing components. Awesome.

Containment is a technique in Component Composition and it is not restricted to only one component, you can contain more than one Component in another Component.

That is it for this post, thank you for reading through. Show me some love by reacting to this post and sharing this post on social media. You can play with the code on CodeSandbox

You can also find me on twitter @solathecoder .

Happy Coding.

Comments (3)

Blaine Garrett's photo

Food for thought: Correct me if I am wrong, but if any Component below App conditionally decides to not render any part of the tree that includes Component3 you will still have the overhead of rendering the virtual dom for ComponentNeedingProps even though it doesn't get put in the dom.

I ran into this a while ago making a Table component. I had the cell renderer part of switch statement. Each time the switch ran, all components were being evaluated until the switch exited. This was happening for every cell of the table. Yikes.

Olusola Samuel's photo

Full Stack Web Developer (MERN)

Real food for thought! I haven't encountered such a situation as you described. I'm convinced you are correct and I've realized I will need to read more on the virtual dom.

However, I'm curious, what did you do in the case of your Table Component?

Blaine Garrett's photo

Software Engineer and Artist

Since a JSX React component is just a JavaScript function under the hood, much like a JS function, you can assign it to a variable. Then you can call it by reference. The function isn't evaluated at assignment time, but rather at call time. In JSX, the act of wrapping it in the angle brackets implicitly calls the underlying function.

Contrived untested example:

let myFunc = Math.add; // Not evaluated yet
if (condition) {
    myFunc = Math. multiply; // Still not evaluated yet
}
return myFunc(...args); // Only now evaluated.

Similarly, for JSX

let MyComponent = Button; // vs. <Button />
if (conditional) {
    MyComponent = CustomButton; // vs. <CustomButton />
}
// No components have been evaluated yet
... Do lots of other stuff
// Component only gets evaluated below
return <MyComponent {...props} />

Note: JSX treats variables that start with lowercase names as html element names rather than variables and will error if it is not valid element.

I have no idea what this does off hand, but it probably errors
// let a = MyComponent 
// return <a {...props}>

I ended up refactoring the table cell renderer code to leverage this concept and conditionally assigned the className and built the props rather than conditionally rendering the JSX.