React Context API: A deep dive with examples

# React

Note

React Context API: A deep dive with examples was originally published on LogRocket.


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.

Prerequisites

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.

Components and props in React

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 prop drilling

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.

Getting started with React Context

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
}

React Context API examples

Storing and accessing a user profile

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:

  • The receiving components are TopNav and Profile
  • The state the receiving components need is in UserProvider
  • All child components of 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 it expects an object with a username:
const [userDetails, setUserDetails] = useState({
  username: "John Doe",
});

Global shared state with React Context

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.

What the React Context API is used for

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.

Redux vs. the React Context API

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.

Using React Context with functional components

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

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.

Using React Context with class-based components

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.

Conclusion

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:

  • The React Context API is designed for prop drilling
  • If you use Context for global state management, use it sparingly
  • If you can’t be prudent with Context, try Redux
  • Redux can be used independently from React
  • Redux is not the only state management tool available

I hope you enjoyed this tutorial!