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

A deep dive into React Context API

6 min read 1920

The React Context API.

It always begins with a single component. You could maybe define a class or functional component, but ultimately you must always be ready to render. You can have data in the form of local state(s), or functionality in form of, say, event handlers.

Your component will grow in responsibility and complexity. No component is a singleton, you learned, so you break your component down.

Now you have two components. Then three. Then four. Eventually you’ll have X number of components, each with its own complexity. The good stuff.

Components let you isolate parts of your large application so you can have a separation of concerns, and if anything breaks you can easily identify where things went wrong. Regardless, components are also meant to be reusable: you want to avoid duplicated logic, and you also want to keep watch for over-abstraction. Reusing components comes with the Don’t Repeat Yourself (Benefits), but it isn’t carved in stone as taught in Goodbye, clean code.

More often than not, components will have some data or functionality that another component needs. This could be to avoid duplication, or to keep the components in synchronization.

Whatever the reason, some components might need to communicate, and the way to do this in React is through props.

Components and props

Components are like JavaScript functions that can accept any number of arguments.

Ideally, a function’s arguments are used for its operation.

With components, these arguments are called props. Props (short for properties) are object arguments. 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. But this is just one component, and this example doesn’t even tell where the message prop came from, which matters.

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

Prop drilling

React is all about updating the DOM of your application whenever it is absolutely necessary. To do this effectively, React uses a virtual DOM (VDOM) to update the actual 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 (without using them) dispatches them to subsequent components SideNav and Main 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 way isn’t technically wrong and is, in fact, the default way to do it. This pattern is known as Prop-drilling.

A diagram will do justice to better illustrate the component hierarchy.An example of a React Component tree.)

App is the initiating prop-passing component and while Apps states: title , username and activeProfileId was passed down as props, the components that needed those props were: SideNav, TopNav and Profile but we had to go through intermediary components: Dashboard, Main, and Page that merely relayed the props.

Traversing from AppDashboard to SideNav is relatively easy compared to AppDashboardMainPageProfile.

Along the chain, anything could go wrong: there could be a typo, refactoring could occur in the intermediary components, possible mutation of these props could happen. And if we remove just one of the intermediary components, things will fall apart.

Apart from all the de-merits mentioned, there’s also a case of re-rendering. Because of the way React rendering works, those intermediary components will also be forced to re-render which can lead to a performance drain of your app.

To understand how React handles rendering, A (mostly) Complete Guide to React Rendering Behavior by Mark Erikson (Redux Maintainer) is a must read.

To solve for the problems attached with prop-drilling came Context.

Context to the rescue

According to the React docs, context provides a way to pass data through the component tree without having to pass props down manually at every level.

The old Context was tedious to use with class components because you had to use the render prop pattern, and more so, it was marked unstable/experimental all the while.

But with the advent of Hooks and specifically the useContext Hook, things are relatively simple today.

After you’ve orchestrated a context, using it is as simple as:

const userDetails = useContext(UserContext);

Check out How (and When) to use React’s new Context API to get started.

Creating a context is straightforward, too. Kent has a bevy of articles and pattern on how to use context and using it effectively. I encourage you to read those articles.

One of my use cases for context is storing the user profile and accessing it wherever I need it.

Apart from that, I can also keep a shared-state in sync. Let’s build a dashboard app again.

User profile in React Context.

The component tree will look something like this:

Another example of a React component tree.

This looks something like the prop-drilling component tree above, except that the username is the only consideration here.

If you take a look at the diagram, you might notice a few things:

  • 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";

const UserContext = createContext(undefined);
const UserDispatchContext = createContext(undefined);

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 UserContext and UserDispatchContext providers with the value prop.

Wrapping UserProvider, as in Main below, will expose the values of UserContext and UserDispatchContext to components down the tree:

function Main() {
  return (
    <div className="dashboardContent">
      <UserProvider>
        <TopNav />
        <Page />
      </UserProvider>
    </div>
  );
}

In Profile, username can be used like so:

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 userDetails it expects an object with a username:

const [userDetails, setUserDetails] = useState({
    username: "John Doe"
});

context-nav-profile-example

context-nav-profile-example by adebiyial using react, react-dom, react-scripts

Global shared state with context

Another use case of context is that it can act as a global state mechanism as we have 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 with context because whenever it renders, its child component(s) also render. One way to minimize the rendering is to keep Context as close to where it’s been used as we’ve done with UserProvider. There’s no reason why we couldn’t position it anywhere high up the component tree, but it’d be less effective.

What context is and isn’t

We’ve already mentioned the React docs definition of Context: Context provides a way to pass data through the component tree without having to pass props down manually at every level.

By this definition alone, it is safe to say that context helps or is meant to solve the props-drilling problem. However, many have orchestrated a state management mechanism from context because by default it is merely another React component – with the added benefit that it helps with prop-drilling.

There is nothing wrong with using context for state management, but keep in mind that this is an indirect way to use it because it isn’t a dedicated state management tool like Redux, and it doesn’t come with sensible defaults.

Moreover, its simplicity is a virtue that should not be taken for granted.

Now that we’ve mentioned what xontext is (the prop-drilling messiah) and what it isn’t (a state management tool), it becomes fair to initiate the salient conversation about whether to use context or Redux, or even if Redux is dead because of context.

Redux or Context

Does context take away Redux? No. It doesn’t. As we have seen, they are two different tools and comparing them is often an indirect consequence of the misconception of what they are.

Context can be orchestrated (with useReducers as Robin wrote in his article How to create Redux with React Hooks?) to act as a state management tool, but that wasn’t the intention and you’d have to a little bit of work to match your taste.

While you’re at that, remember there are already a lot of state management tools out there that work well and will ease your troubles.

In my experience with Redux, it was relatively complex for something that is easier to solve today with context. But keep in mind, this is just where their paths cross: prop-drilling and global state management. Redux is much more than this.

As Dan pointed out, you might not need Redux and even if you do, you’d know when you need it. And not knowing when to add another context is one of the pitfall of using context.

In a just and fair world, Redux and context shouldn’t be two words/concepts stringed together with “vs”, and instead should be considered things that work with each other.

I’d say use Redux for your complex global state management and context for prop-drilling. If you think about what the world is today, I bet we can use some co-existence.

Conclusion

The takeaway here is that:

  • Context is meant for prop-drilling
  • If you’d use Context for some global state management, be prudent with it
  • If you fear you can’t be prudent with Context, reach out for Redux
  • Redux can be used independent of React
  • Redux is not the only state management tool out there

Resources

React’s new Context API
Prop Drilling
How to use React Context effectively
How to optimize your context value
React State Hooks: useReducer, useState, useContext
That React Component Right Under Your Context Provider Should Probably Use React.memo
Fix the slow render before you fix the re-render
React, Redux, and Context Behavior

 

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult 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.

Leave a Reply