Editor’s note: This article was last updated on 24 March 2023 to compare the process of using React Context with class-based components vs. functional components. Check out this article to ensure that you don’t overuse React Context.
Despite React’s popularity, one of the biggest obstacles developers face when working with the library is components re-rendering excessively, slowing down performance and harming readability. Component re-rendering is especially damaging when developers need components to communicate with each other in a process known as prop drilling.
The React Context API, introduced in React v.16.3, allows us to pass data through our component trees, giving our components the ability to communicate and share data at different levels. In this tutorial, we’ll explore how we can use React Context to avoid prop drilling. First, we’ll cover what prop drilling is and why we should avoid it.
This article assumes you have a solid grasp of JavaScript and an intermediate level knowledge of React itself. While this article is easy to follow and understand, we won’t provide detailed explanations of basic JavaScript and React concepts as we go through examples.
While your application might start out with just a single component, as it grows in complexity, you must continually break it up into smaller components. With components, we can isolate individual parts of a larger application, providing a separation of concern. If anything in your application breaks, you can easily identify where things went wrong using fault isolation.
However, components are also meant to be reusable. You want to avoid duplicate logic and prevent over-abstraction. Reusing components comes with the benefits of DRY code; components usually have some data or functionality that another component needs, for example, to keep components in synchronization. In React, we can use props to make our components communicate.
Components are like JavaScript functions that can accept any number of arguments. Ideally, a function’s arguments are used for its operation. I like to think of a function as a block of code that performs a function with either zero or any number of arguments passed to it. For example, take the following function sum
that adds two numbers, a
and b
:
function sum(a, b) { return a + b; }
Executing the function is fairly straightforward:
console.log(sum(1, 2)); // 3
In React components, these arguments are called props, short for properties. An ErrorMessage
can look something like this:
function ErrorMessage(props) { return ( <div className="error-message"> <h1> Something went wrong </h1> <p> {props.message} </p> </div> ) }
Because ErrorMessage
will be reused many times across the app, it will pass a different message
in its props. However, this is just one component, and this example doesn’t clarify where the message
prop came from, which is important for us to know.
React keeps UI changes in the virtual DOM, then updates the browser DOM through a process known as reconciliation. Let’s take a simple dashboard app as an example:
function App() { const [title, setTitle] = React.useState("Home"); const [username, setUsername] = React.useState("John Doe"); const [activeProfileId, setActiveProfileId] = React.useState("A1B2C3"); return ( <div className="app"> <h1>Welcome, {username}</h1> <Dashboard {...{ activeProfileId, title, username}}/> </div> ) }
The App
component has three states
: activeProfileId
, title
, and username
. The states
have default values, and they are passed down to the Dashboard
component:
function Dashboard({activeProfileId, title, username}) { return ( <div className="dashboard"> <SideNav {...{activeProfileId}}/> <Main {...{title, username}}/> </div> ) }
The Dashboard
component receives the props
and immediately dispatches them to subsequent components SideNav
and Main
further down the tree:
function SideNav({activeProfileId}) { return ( <nav className="side-nav"> <h1>ID: {activeProfileId}</h1> </nav> ) } function Main({title, username}) { return ( <div className="main-content"> <TopNav {...{title}}/> <Page {...{username}}/> </div> ) } SideNav immediately consumes the activeProfileId prop, and Main continues to relay the title and username props further down the tree. function TopNav({title}) { return ( <nav className="top-nav"> <h1> {title} </h1> </nav> ) } function Page({username}) { return <Profile {...{username}}/> } TopNav uses the title props, and Page sends username down, again, to Profile: function Profile({username}) { return <h1>{username}</h1> }
Finally, Profile
uses the username
props. Passing props down in this manner, known as prop drilling, is the default method. To better illustrate the component hierarchy, view the diagram below:
App
is the initiating prop-passing component. While App
‘s states title
, username
, and activeProfileId
were passed down as props
, the components that needed those props
were SideNav
, TopNav
, and Profile
. However, we had to go through intermediary components Dashboard
, Main
, and Page
, which merely relayed the props.
Traversing from App
to Dashboard
to SideNav
is relatively easy compared to navigating from App
, Dashboard
, Main
, Page
, and finally to Profile
. Along the chain, anything could go wrong. For example, there could be a typo, refactoring could occur in the intermediary components, or our props
might experience a mutation. Also, if we remove a single intermediary component, the whole process will fall apart.
There is also the issue of re-rendering. Because of the way React rendering works, intermediary components will also be forced to re-render, degrading your app’s overall performance. Let’s see how we can solve these problems using the React Context API.
According to the React docs, React Context provides a way to pass data through the component tree from parent to child components, without having to pass props down manually at each level.
Each component in Context is context-aware. Essentially, instead of passing props down through every single component on the tree, the components in need of a prop can simply ask for it, without needing intermediary helper components that only help relay the prop.
We’ll use the useContext
Hook to create and use a new Context as follows:
// import UserContext — you'd learn how to implement this below function UserProfile() { const userDetails = useContext(UserContext); // rest of the component }
One of my favorite use cases for Context is storing a user profile and accessing it wherever I need to. I can also keep a shared state in sync. Let’s build our dashboard app again:
The component tree will look something like this:
Notice that the diagram looks similar to the prop-drilling component tree above, except username
is the only consideration. You might also notice the following:
TopNav
and Profile
UserProvider
UserProvider
have direct access to the username
state, including TopNav
, Page
, and Profile
Direct access means that even though Page
is a parent component to Profile
, it doesn’t have to be an intermediary component anymore:
import React, { createContext, useState } from "react"; // Create two context: // UserContext: to query the context state // UserDispatchContext: to mutate the context state const UserContext = createContext(undefined); const UserDispatchContext = createContext(undefined); // A "provider" is used to encapsulate only the // components that needs the state in this context function UserProvider({ children }) { const [userDetails, setUserDetails] = useState({ username: "John Doe" }); return ( <UserContext.Provider value={userDetails}> <UserDispatchContext.Provider value={setUserDetails}> {children} </UserDispatchContext.Provider> </UserContext.Provider> ); } export { UserProvider, UserContext, UserDispatchContext };
The state variables userDetails
and setUserDetails
are exposed through the UserContext
and UserDispatchContext
providers with the value
prop.
Wrapping UserProvider
, as in Main
below, will expose the value
props of UserContext
and UserDispatchContext
to the TopNav
and Page
components down the tree:
function Main() { return ( <div className="dashboardContent"> <UserProvider> <TopNav /> <Page /> </UserProvider> </div> ); }
In Profile
, we can use username
as follows:
function Profile() { const userDetails = React.useContext(UserContext); const setUserDetails = useContext(UserDispatchContext); return <h1> {userDetails.username} </h1>; } // setUserDetails is a function as de-structured. When using it to update userDetail// s it expects an object with a username: const [userDetails, setUserDetails] = useState({ username: "John Doe" });
Another use case for React Context is using it as a global state mechanism, like we have in between TopNav
and Profile
. Updating the username
in Profile
immediately updates the shared state in UserProvider
, providing a mechanism for global state management.
As with prop drilling, there can be some performance drain when using Context. Whenever it renders, its child components also render. One way to minimize rendering is to keep Context as close to where it’s being used as possible, like we’ve done with UserProvider
. Although we could position it higher up in the component tree, it would be less effective.
With React Context, we can pass data deeply. While some developers may want to use Context as a global state management solution, doing so is tricky. While React Context is native and simple, it isn’t a dedicated state management tool like Redux, and it doesn’t come with sensible defaults.
If you decide to use React Context at all, you should be aware of its potential for performance drain. You can very easily get carried away and add too many components where they aren’t needed. To prevent re-rendering, be sure to place contexts correctly only in the components that require them.
Does React Context replace Redux? The short answer is no, it doesn’t. As we’ve seen, Context and Redux are two different tools, and comparison often arises from misconceptions about what each tool is designed for. Although Context can be orchestrated to act as a state management tool, it wasn’t designed for that purpose, so you’d have to do put in extra effort to make it work. There are already many state management tools that work well and will ease your troubles.
In my experience with Redux, it can be relatively complex to achieve something that is easier to solve today with Context. Keep in mind, prop drilling and global state management is where Redux and Context’s paths cross. Redux has more functionality in this area. Ultimately, Redux and Context should be considered complementary tools that work together instead of as alternatives. My recommendation is to use Redux for complex global state management and Context for prop drilling.
Functional components are quite popular among React developers because they’re lightweight and simpler to use than their alternative, which are class-based components.
First, to avoid confusion, let’s create a new context file called MyContext.js. This will return an object that contains both a Provider
and a Consumer
component:
import React from 'react' const MyContext = React.createContext({}) export const MyProvider = MyContext.Provider export default MyContex
Next, we’ll wrap the parts of our application that need access to the context with the Provider
component. You can set the value of the context using the value
prop on the Provider
. In this case, we’ll be giving the Provider
a name and age value:
import React from 'react' import { MyProvider } from '../MyContext' function App() { return ( <MyContext.Provider value={{ name: 'Charlie', age: 40 }}> <MyComponent /> </MyContext.Provider> ); }
Finally, in the functional component that needs access to the context, we’ll use the useContext
Hook to retrieve the value of the context, like this:
import React, { useContext } from 'react' import MyContext from '../MyContext' function MyComponent() { const { name, age } = useContext(MyContext); return ( <div> <h1>My name is {name}.</h1> <h2>I am {age} years old.</h2> </div> ); }
Now, MyComponent
will have access to the name and age values that were set in the context by the Provider
component. The key part here is the useContext
Hook, which we use to import MyContext
into the functional component.
A class-based component in React is a type of component that is defined using a JavaScript class. It’s one of the two main ways to define a component in React, the other being a functional component, which we just covered above.
Using the context in class-based components is similar to using it in functional-based components, except for a few syntax changes. If we want to import the context we used in the previous second, we’ll do that by using a provider and then giving it a value:
import React from 'react' import { MyProvider } from '../MyContext' class App extends React.Component { render() { return ( <MyContext.Provider value={{ gender: 'John', occupation: 25 }}> <MyComponent /> </MyContext.Provider> ); } }
We imported the context into the App
class component and then wrapped the other components with the context. To consume it in the MyComponent
component, we’ll use the consumer keyword, like this:
import React from 'react'; import MyContext from '../MyContext' class MyComponent extends React.Component { render() { return ( <MyContext.Consumer> {({ gender, occupation }) => ( <div> <h1>I identify as a {gender}.</h1> <h2>I work as a {occupation}.</h2> </div> )} </MyContext.Consumer> ); } }
With that, we’ve successfully used React Context in a class-based component. You can also consume the context in class-based components by using this.context
and declaring it as a static contextType
, like this:
import React from ‘react’; import MyContext from ‘./MyContext’; class MyComponent extends React.Component { static contextType = MyContext; render() { const { gender } = this.context.gender; return <div>I identify as a {gender}</div>; } }
The only problem with using this method is that we can only set the static contextType
once, so if we need to use more than one context, it would be impossible to do that.
In this article, we reviewed what the React Context API is, when we should use it to avoid prop drilling, and how we can use Context most effectively. We also cleared up some misconceptions surrounding the React Context API and Redux.
The main takeaways from this article include the following:
I hope you enjoyed this tutorial!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe use cases for the ResizeObserver API may not be immediately obvious, so let’s take a look at a few practical examples.
Automate code comments using VS Code, Ollama, and Node.js.
Learn to build scalable micro-frontend applications using React, discussing their advantages over monolithic frontend applications.
Build a fully functional, real-time chat application using Laravel Reverb’s backend and Vue’s reactive frontend.
11 Replies to "React Context API: A deep dive with examples"
What am I doing wrong? When I try, I get this error “Objects are not valid as a React child (found: object with keys…” Using react Version 17.x
“`
function UserProvider({children}) {
const value = useState({
name: ‘Guest’,
email: false,
is_logged_in: false,
is_admin: false
});
return {children}
}
“`
That’s because children isn’t a React component.
It deleted most of the code that I pasted
I’d like to get more context of your code. Can you create a reduced-test-case (https://css-tricks.com/reduced-test-cases/) and push to GitHub?
Great article! I’ve considered using context for form validation (i.e. validation errors from the server) so that all children (inputs) of a form can show validation errors without passing the errors array to each input. Redux (or similar) isn’t really appropriate here since there can be multiple forms on a page (at least we’ll need to identify each) and that validation errors are only relevant for descendants.
Thanks mate. Before doing that, have you tried Formik? https://css-tricks.com/using-formik-to-handle-forms-in-react/
Hi, In the Profile() method, how do I set the username? setUserDetails({username: “known-user”}) doesn’t seem to work.
Hey. What’s the error message you’re getting?
This is an amazing explanation. Thanks!
Traditionally, this is the case for all the reasons mentioned. Though you can try @webkrafters/react-observable-context on npm. It removes many of the redux and react context bottlenecks while making it easier to reuse your components.
Also, instead of having two different contexts for passing down a value and setting the value, you can have this in one function and pass the value as an object containing the actual value and function which will update the value. For example, in your example:
“`
import React, { createContext, useState } from “react”;
const UserContext = createContext(undefined);
const UserDispatchContext = createContext(undefined);
function UserProvider({ children }) {
const [userDetails, setUserDetails] = useState({
username: “John Doe”
});
return (
{children}
);
}
“`
we can have this as:
“`
import React, { createContext, useState } from “react”;
const UserContext = createContext(undefined);
function UserProvider({ children }) {
const [userDetails, setUserDetails] = useState({
username: “John Doe”
});
return (
{children}
);
}
“`
then in the component that uses this prop, obtain the values as:
“`
const {userDetails, setUserDetails} = useContext(UserContext);
“`