Adebiyi Adedotun Caught in the web, breaking things and learning fast.

React Context API: A deep dive with examples

6 min read 1825

react-context-api-deep-dive

Editor’s note: This post was last updated in October 2021 to reflect the most recent information about the React Context API.

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 new React Context API, introduced with 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.

Table of contents:

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:

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:

We made a custom demo for .
No really. Click here to check it out.

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:

React Prop Drilling Dashboard Example

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

React Context API Dashboard Example

The component tree will look something like this:

React Context API Dashboard Component Tree

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// s 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 a lot of 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 are.

Ultimately, Redux and Context should be considered complementary tools that work together instead of alternatives. My recommendation is to use Redux for complex global state management and Context for prop drilling.

Conclusion

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

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. I hope you enjoyed this tutorial!

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Adebiyi Adedotun Caught in the web, breaking things and learning fast.

4 Replies to “React Context API: A deep dive with examples”

  1. 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}
    }
    “`

Leave a Reply