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:
By declaring the
Consumer
component, which returns a function, whose argument will be the value passed from theProvider
, like the example above.Using the hook
useContext()
, it takes context component as an argument, returns the value from theProvider
. 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 :)
Top comments (5)
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?
"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."
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.
Nice!