How to avoid Prop Drilling in REACT

What is Prop Drilling?

Prop drilling occurs when a parent component generates its state and passes it down as props to its children components that do not consume the props – instead, they only pass it down to another component that finally consumes it.

Below is an example of prop drilling in React:

function App() {
  const [profile, setProfile] = useState({ame: 'John'}); 
  return ( 
    <div> <Header profile={profile} /> 
    </div> 
  ); 
} 

function Header({ profile }) { 
  return ( 
    <header> 
      <h1>This is the header</h1> 
      <Content profile={profile} /> 
    </header> 
  ); 
} 

function Content({ profile }) { 
  return ( 
    <main> 
      <h2>Content Component</h2> 
      <p>{profile.name}</p> 
    </main> 
  ); 
} 

export default App;

If you check out the example above, you'll notice that profile is passed from the App component through the Header to the Content component, which eventually makes use of the props. This is commonly referred to as prop drilling as the Header component doesn't consume the prop but only passes it down to the Content component that finally consumes it.

Now that you understand what prop drilling is, the next challenge is to figure out how to avoid it because it's not always an intuitive process.

How to Fix Prop Drilling with Component Composition

Component composition is a good approach to fix prop drilling. If you ever find yourself in a situation where a component passes down a prop it neither creates nor consumes, you can use component composition to fix it.

But to use component composition, you need to understand a component context.

What is a component context?

The context of a component encompasses everything that is visible within it, including state, props, and children. The following code further illustrates this concept:

function App() { 
  const [profile, setProfile] = useState({name: 'Ayobami'}); 
  return ( 
    <div> 
      <Header profile={profile} /> 
    </div> 
  ); 
} 

export default App;

In this scenario, the context of App refers to everything we can see within the App component – including the profile prop, the Header, and other App content. Therefore, any data created in the App component should ideally be utilized within the App component itself, either as its own data or as props to its children.

Prop drilling always emerges when the children receiving the props doesn't consume it but only passes it down to its children.

To avoid prop drilling in this case, any grandchildren components that require access to the same props, especially when their parent don't consume the data, should be passed as children ensuring that the data remains within the App context.

export function App() { 
  const [profile, setProfile] = useState({name: 'Ayobami'}); 
  return ( 
    <div> 
      <Header> 
        <Content profile={profile} /> 
      </Header> 
    </div> 
  ); 
}

or

export function App() { 
  const [profile, setProfile] = useState({name: 'Ayobami'}); 
  return ( 
    <div> 
      <Header children={<Content profile={profile} />} > 
    </div> 
  ); 
}

As you can see, we have resolved the prop drilling issue in the previous example, even though we still have a redundant component, right? We've successfully addressed prop drilling through component composition.

This process is quite straightforward because we concentrate on recognizing elongated props and repositioning them within appropriate contexts.

The concept of prop drilling is problem-focused, but prop elongation is solution-driven. When dealing with elongated props, our primary goal is to identify props that are not consumed but only passed down to another components.

How to Fix Prop Drilling by Moving State to the Consumer

Prop drilling can also be fixed by moving state to where it is consumed. The example of prop drilling in this article has a component named Content. But the component is forced to receive a prop from its parent instead of having a state and be an independent component – and so we have prop drilling.

We can fix the prop drilling in this case by moving the profile state to where it is consumed.

Let's revisit the example:

function App() {
  const [profile, setProfile] = useState({ame: 'John'}); 
  return ( 
    <div> 
      <Header profile={profile} />
      <Footer profile={profile />
    </div> 
  ); 
} 

function Header({ profile }) { 
  return ( 
    <header> 
      <h1>This is the header</h1> 
      <Content profile={profile} /> 
    </header> 
  ); 
} 

function Content({ profile }) { 
  return ( 
    <main> 
      <h2>Content Component</h2> 
      <p>{profile.name}</p> 
    </main> 
  ); 
} 

export default App;

Now that we have lifted the profile to the Content component where it is consumed, the App component doesn't have a state, while the Header component doesn't receive a prop again as the Content component has its state.

But wait! There is a problem. The Footer component needs the state we moved away from App. There you are! That is the problem with lifting or moving state to where we think it is needed. In this case, if the Footer component doesn't need it, we won't have any issue – but Footer also needs the prop.

Now that Footer needs profile as a prop, we need to solve prop drilling with another method.

How to Fix Prop Drilling with a Children-Replacing-Parent Strategy

Earlier in this article, we talked about how to use component composition and moving state to its consumer to solve prop drilling. But as you saw, they have some issues – duplicated components or states.

But using this children-replacing-parent approach fixes the problem effectively:

Working but could be better:

export function App() { 
  const [profile, setProfile] = useState({name: 'Ayobami'}); 
  return ( 
    <div> 
      <Header> 
        <Content profile={profile} /> 
      </Header> 
    </div> 
  ); 
}

function Header({ profile }) { 
  return ( 
    <header> 
      <h1>This is the header</h1> 
      <Content profile={profile} /> 
    </header> 
  ); 
}

The example above shows a solution to the prop drilling example in this article. But as you can see, it has a redundant component, as Header does nothing.

Here's a better version:

export function App() { 
  const [profile, setProfile] = useState({name: 'Ayobami'}); 
  return ( 
    <header> 
      <h1>This is the header</h1> 
      <Content profile={profile} /> 
    </header> 
  ); 
}

In the above code, we enhance the component composition solution we previously implemented for the prop drilling example by replacing the redundant Header component with its content in its parent (App).

What to Avoid

It's essential to highlight what to avoid when dealing with prop drilling to prevent unnecessary challenges.

  • Avoid React Context, if possible, to fix prop drilling. This approach ties your component to a specific context, restricting its usability outside of that context and hindering composition and reusability.

  • Steer clear of redundant components by employing a children-parent replacement approach. This approach naturally incorporates component composition without introducing redundant components or states when resolving prop drilling.

By avoiding elongated props, you pave the way for crafting maintainable, high-performing, reusable, and scalable React components. It simplifies the process of lifting states and components by removing the struggle of deciding where to place them.

With your understanding of elongated props, you can confidently position props and components within the right context without undue stress.

In short, you can now discover prop drilling intuitively by paying attention to any component that takes props it doesn't consume and only passes it down to another component.

Thanks for reading – cheers!