This page looks best with JavaScript enabled

Prop Drilling in React: How to Avoid It

 ·  ☕ 7 min read  ·  ✍️ Iskander Samatov

react avoid prop drilling


In this post, I’ll show you strategies to avoid creating a prop drilling mess in your React code. I will also provide some tips for better organizing and maintaining your components.

Refactoring

If you find yourself drilling props often, that can be a signal to reconsider your application’s data flow or decision tree and an opportunity to refactor.

Lift state up

Lifting the state up in the component tree can help minimize prop drilling.

You remove the need to pass more props down to child components by lifting the state that depends on those props to the parent. It also makes the state more flexible and easier to share between multiple child components.

Say you have a list of usernames that you want to display. Each username item decides whether it should render its content using the props you pass:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const UsernameListItem = ({ selectedUserId, item }) => {
  if (selectedUserId !== item.id) {
    return null;
  }

  return <div>{item.username}</div>;
};

const List = ({ items, selectedUserId }) => {
  return (
    <div>
      {items.map((item) => (
        <UsernameListItem item={item} selectedUserId={selectedUserId} />
      ))}
    </div>
  );
};

Should it really be up to UserNameListItem to decide whether to render itself? Instead of passing multiple props to UsernameListItem, you can decide whether to render it from within the List component.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const UsernameListItem = ({ item }) => {
  return <div>{item.username}</div>;
};

const List = ({ items, selectedUserId }) => {
  return (
    <div>
      {items.map((item) =>
        item.id === selectedUserId ? <UsernameListItem item={item} /> : null
      )}
    </div>
  );
};

Now we no longer need to worry about passing the selectedUserId prop to UserNameListItem.

This approach also enforces a good separation of concerns: The child only cares about rendering the username, and the parent decides when to render the child.

Making props more flexible

Another way to avoid prop drilling is to make your props more flexible. I often see this pattern with React components: the component has a long list of props. Most of those props are booleans that dictate whether or not to display a specific parent of the UI for one reason or another.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const FileControls = ({ canWrite, canCopy, canDelete, file }) => {
  return (
    <div>
      <span>{file.title}</span>
      <button>View</button>
      {canWrite && <button>Edit</button>}
      {canCopy && <button>Copy</button>}
      {canDelete && <button>Delete</button>}
    </div>
  );
};

This problem doesn’t appear immediately, but when the number of different ways to display the UI starts to grow, it can quickly become unmanageable. When you see a long list of props like this, it’s a good sign that component is due for some refactoring.

What if instead of providing canWrite, canCopy, and canDelete, we provide an array of actions the user can take?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const FileControls = ({ availableActions, file }) => {
  return (
    <div>
      <span>{file.title}</span>
      {availableActions.map((action) => (
        <button>{action.title}</button>
      ))}
    </div>
  );
};

This way, we can add, remove, or change the order of actions without affecting the props. And the child component doesn’t need to know about those actions, it only needs to render the ones it receives.

Context API or global management solutions

There are a lot of great online posts on this topic, so we won’t go into too much detail here.

But briefly, Context API (or a global management solution like Redux) is considered a to-go solution for solving the prop drilling problem.

By putting your data in a global store, you can access it from anywhere in your application without having to pass props down the component tree.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const ThemeContext = React.createContext("dark");

class App extends React.Component {
  render() {
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

const ThemedButton = () => {
  const themedContext = useContext(ThemeContext);

  return <Button theme={themedContext} />;
};

Notice how we don’t need to pass the theme prop through all the components in order for the ThemedButton to have access to it.

This pattern can be helpful when you have data used in multiple places throughout your application.

Cons of using the Context API

While using a global state is often a valid solution to avoid prop drilling, it’s important to note that you don’t want to use a global store for everything. Only the data that is truly global (or necessary) should be put in there. Otherwise, you’ll end up with a bloated store and an application that is difficult to reason about.

Also, your components become reliant on the context, which makes them harder to test and reuse.

There is also a performance cost associated with using Context, so you want to make sure not to use it for data that’s updated often, like users typing their input. That will cause the whole component tree to re-render on each keystroke, which will have a visible impact on the performance.

Component composition

Component composition is a way of avoiding prop drilling using proven principles that will also make your code easier to read and maintain.

With component composition, you build complex components on top of simpler ones.

There are two main patterns in component composition: Container pattern and specialized components.

Container pattern

The container pattern is a variation of the render prop pattern, which you might have seen before. The container is a wrapper component with distinct functionality and can be wrapped around any other component.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import React from "react";
import { useGetUserSession } from "./useGetUserSession";

const GateComponent = ({ children }) => {
  const { isAuthenticated } = useGetUserSession();

  if (!isAuthenticated) return null;

  return <>{children}</>;
};

export default GateComponent;

In this case, we have a GateComponentcontainer that uses a useGetUserSession hook to get the authentication state from our application. The GateComponent will only render its children if the user is authenticated.

With this pattern, you don’t need to declare your child component inside your container. Instead, you can simply wrap it with the container wrapper, thus avoiding at least one level of prop drilling.

Specialized components

Specialized components are generic components with the value of their props predefined ahead of time. In other words, they are generic components tailored for a specific use case.

For example, say we have a generic ListItem component:

1
2
3
4
5
6
7
8
const ListItem = ({ text, onClick, leftComponent }) => {
  return (
    <div onClick={onClick}>
      {leftComponent}
      <span>{text}</span>
    </div>
  );
};

We want to build a specialized UserListItem component that will always display the user’s name and avatar. To do that, We can create a new component and use our generic ListItem in it.

1
2
3
4
5
6
7
8
9
const UserListItem = ({ user }) => {
  return (
    <ListItem
      text={user.username}
      onClick={() => user.displayProfile}
      leftComponent={<img src={user.avatar} />}
    />
  );
};

Now we have a reusable component that will display the name and avatar. We don’t need to worry about passing the props to ListItem ourselves, the UserListItem component will take care of it. All we need to do is provide the user prop.

Check my other post to learn more about component composition .

Compound components

Compound components are a step further from the component composition. It’s a technique for building complex components using simple building blocks where each of those blocks shares the implicit state of the parent component.

You have probably seen this technique when working with component libraries that provide components like Accordion, Tabs, or Carousel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
export default function App() {
  return (
    <Accordion>
      <AccordionItem id="1">
        <AccordionHeader>Header 1</AccordionHeader>
        <AccordionPanel>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
          eiusmod tempor incididunt ut labore et dolore magna aliqua.
        </AccordionPanel>
      </AccordionItem>
      <AccordionItem id="2">
        <AccordionHeader>Header 2</AccordionHeader>
        <AccordionPanel>
          Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
          nisi ut aliquip ex ea commodo consequat.
        </AccordionPanel>
      </AccordionItem>
    </Accordion>
  );
}

Compound components are powerful because they allow you to build complex pieces of UI in a declarative way without doing much prop drilling.

If you would like to learn more, check out my post on building compound components.

Conclusion

And that’s it for this post. We covered different techniques and approaches for avoiding prop drilling and their pros and cons.

If you’d like to get more web development, React and TypeScript tips consider following me on Twitter, where I share things as I learn them.
Happy coding!

Share on

Software Development Tutorials
WRITTEN BY
Iskander Samatov
The best up-to-date tutorials on React, JavaScript and web development.