loading...

Avoid Prop Drilling In React With Context API

spukas profile image Linas Spukas ・3 min read

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>

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);

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>

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>
  );
}

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>;
}

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;

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>
  );
}

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>;
}

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 :)

Posted on Nov 24 '19 by:

spukas profile

Linas Spukas

@spukas

Full-stack web developer with a specialisation in React and NodeJS.

Discussion

markdown guide
 

Hi Linas, nice article and helped me complete a recent project.

May I suggest a typo fix in the 'Avoid Prop Drilling' paragraph: Using a quite simple Context API, we skip writing props manually at every component level and use the props only where you need to. I think it makes sense and bloats the components less.
Going back to my specific obstacle at work, where we need to pass a handful of actions to the last children in the tree, we pass all the actions to the context:
.

Overall good article, I'm following

 

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?

 
 

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

 

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.