DEV Community

Linas Spukas
Linas Spukas

Posted on

Avoid Prop Drilling In React With Context API

React passes data to child components via props from top to bottom. While there are few props or child components, it is easy to manage and pass down data. But when the application grows, and you start to nest more child components, passing props through middle components, when they do not use props, becomes cumbersome and painful.

Prop drilling problem happens quite often in my daily work. We have a convention for structuring React components, where the top parent component is responsible only for business logic and calling only actions, the second layer is data container, where we fetch and remap data, and pass down to dumb view components:

<Controller> // Responsible for business logic - calling actions
  <DataContainer> // Responsible for combining/fetching data
    <View> // Rendering data and visuals
      <MoreView />
      <MoreView />
    </View>
  </DataContainer>
</Controller>
Enter fullscreen mode Exit fullscreen mode

The problem arises from having a lot of actions inside the controller component that we need to pass to the most distant children in the view components. Passing down all the action functions is very irritating and bloats the components, especially those that do not use these props.

Context API

The Context API solves some of these prop drilling problems. It let you pass data to all of the components in the tree without writing them manually in each of them. Shared data can be anything: state, functions, objects, you name it, and it is accessible to all nested levels that are in the scope of the context.

Provide The Context

To create a context, we need to initialize it:

export const MyContext = React.createContext(defaultValue);
Enter fullscreen mode Exit fullscreen mode

The context can be initialized in the top parent components, or in the separate file; it doesn't matter. It can be imported or exported.
The default value is used when context components cannot find the Provider above in the tree; for example, it was not declared like it supposed to: <MyContext.Provider value={...}>.

For the data to be accessible for all the child components in tree, a context Provider with a value property should be declared and wrap all the components:

<MyContext.Provider value={{ user: 'Guest' }}>
  <View>
    // Deep nested
    <ChildComponent />
  </View>
</MyContext.Provider>
Enter fullscreen mode Exit fullscreen mode

Every component under the MyContext will have an access to the value property.

Consume The Context

The child components will not have direct access to the value, while it is not subscribed to the MyContext. To subscribe to the context, we need to declare a Consumer component. Let's say we have a child component deeply nested in the context tree, in a separate file. We would need to import MyContext and use MyContext.Consumer component:

// ChildComponent.js
import { MyContext } from './MyContext.js'

function ChildComponent() {
  return (
    <MyContext.Consumer>
      {({ user }) => {
        // user is equal to 'Guest' declared above
        return <p>Hello, {user}</p>
      }}
    </MyContext.Consumer>
  );
}
Enter fullscreen mode Exit fullscreen mode

Functional components can subscribe to the data in two ways:

  1. By declaring the Consumer component, which returns a function, whose argument will be the value passed from the Provider, like the example above.

  2. Using the hook useContext(), it takes context component as an argument, returns the value from the Provider. The same example as above with the hook:

// ChildComponent.js
import { MyContext } from './MyContext.js'

function ChildComponent() {
  const context = React.useContext(MyContext);

  return <p>Hello, {context.user}</p>;
}
Enter fullscreen mode Exit fullscreen mode

Class components will consume the context data by assigning context component to the class property contextType:

// ChildComponent.js
import { MyContext } from './MyContext.js'

class ChildComponent extends React.Component {
  render() {
    return <p>Hello, {this.context.user}</p>;
  }
}

ChildComponent.contextType = MyContext;
Enter fullscreen mode Exit fullscreen mode

Avoid Prop Drilling

Using a quite simple Context API, we are able can skip writing props manually at every component level and use the props only where you need to. I think it makes sense and less bloats the components.
Going back to my the specific obstacle at work, where we need to pass a handful amount of actions to the last children in the tree, we pass all the actions to the context:

// Controller.js
import { setLabelAction, setIsCheckedAction } from './actions';

export const ActionContext = React.createContext();

function Controller() {
  const actions = {
    setLabel: (label) => setLabelAction(label),
    setIsChecked: (isChecked) => setIsCheckedAction(isChecked),
  };

  return (
    <ActionContext.Provider value={actions}>
      <DataContainer>
        <View>
          <MoreView />
          ...
    </ActionContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Extract and use actions in the functional components using hooks:

import { ActionContext } from './Controller.js'

export function MoreView() {
  const actions = React.useContext(ActionContext);

  return <button onClick={() => actions.setIsChecked(true)}>Check</button>;
}
Enter fullscreen mode Exit fullscreen mode

Sum Up

Context API is pretty simple and easy to use, can pass any data down the component tree. But need to take into consideration, that abusing it will make your components less reusable because they will be dependent on the context. Furthermore, when parent component rerenders, it might trigger some unnecessary rerendering in the consumer component, because a new value object is created during the updates. Other than that, it is a great tool to share data and avoid prop drilling :)

Latest comments (5)

Collapse
 
slowwie profile image
Michael

So I have to create a new file context.js and import it? Or how can I import it into the childcomponents when I don't have a seperate file?

Collapse
 
hadeel_salah profile image
Hadeel Salah

"The context can be initialized in the top parent components, or in the separate file; it doesn't matter. It can be imported or exported."

Collapse
 
redefinered profile image
Red De Guzman

Nice!

Collapse
 
bburke4 profile image
bburke4

Thanks for the article! What do the actions functions look like? Are you using redux at the top where the context provider is set?

Collapse
 
spukas profile image
Linas Spukas

Hi, thanks. In my case, we are using Meteor.js framework, which has specific methods, that modifies data in the server and client. So actions are wrapped around these methods.