React Context API

Published on May 22, 2022

The Context API in React is a tool that allows you to share state among components in a clean and easy manner. It can be especially useful when you have state defined in a parent component that is needed in a deeply nested component, or when you have state that is required by multiple components, avoiding the need to repeat the code multiple times. It allows you to “magically transport” data wherever it is needed without having to pass it through multiple components using prop-drilling(passing data through many components just to get it to one component that is deeply nested).

The Context API is often compared to Redux, a more comprehensive state management library that offers additional features such as Redux devtools and easy ways to modify state. It’s not uncommon to find React apps that use both the Context API and Redux in the same app, depending on the specific state needs.

This article will show you how to get started with the Context API for functional components, assuming you already have a basic understanding of React fundamentals. Let’s consider a simple app called TodoManager, with three small components. The top-level parent component is called App, and it has two states defined: color and done These states are not needed in the List component, but they are needed in the Todo component.

App.js
import { useState } from "react"
import List from "./List"

function App() {
  const [done, setDone] = useState(false)
  const color = done ? "green" : "black"

  return (
    <>
      <h1 className="class">Tasks</h1>
      <label>
        <input type="checkbox" onChange={e => setDone(e.target.checked)} />
        Done
      </label>
      <hr />
      <List color={color} done={done} />
    </>
  )
}

export default App

List is used to render the list of todos.

List.js
import Todo from "./Todo"
import data from "./data.json"

export default function List({ color, done }) {
  const todoItems = data.todos.map(todo => (
    <li key={todo.id}>
      <Todo todo={todo} color={color} done={done} />
    </li>
  ))

  return <ul>{todoItems}</ul>
}

Todo handles how a single todo will be displayed.

Todo.js
export default function Todo({ todo, color, done }) {
  return (
    <p style={{ color }}>
      <label>
        <input type="checkbox" checked={done} readOnly={true} />
      </label>
      {todo.title} - {todo.owner}
    </p>
  )
}

Currently, we are using prop-drilling to pass state through multiple intermediate components just to get it to the bottom-level component. This can become difficult to manage, especially in large codebases with complex logic. The Context API offers a solution to this issue by allowing us to refactor the app so that we don’t have to pass the done and color states through unnecessary components. Instead, we can consume them directly where they are needed.

How To use Context

Below are the steps to follow when using react’s context API.

Step 1. Create the context

We use the createContext function to achieve this, which accepts an optional default value. For example, we can create a context for the color information with a default value of blue.

ColorContext.js
import { createContext } from "react"

export const ColorContext = createContext("blue")

createContext does exactly what its name says. It also returns on abject which includes two key components: Provider and Consumer . These are discussed in the following sections.

Step 2: Use the context

Now that we have created the context, we can use it in the child component where the color state is actually needed. This allows us to stop passing the state through prop-drilling and consume it directly from where it is needed:

Todo.js
import { useContext } from "react"
import { ColorContext } from "./ColorContext"

function Todo({ todo, color, done }) {
  const { color } = useContext(ColorContext)
  return (
    <p style={{ color }}>
      <label>
        <input type="checkbox" checked={done} readOnly={true} />
      </label>
      {todo.title} - {todo.owner}
    </p>
  )
}

At this point, we are no longer the passing color prop down through the List component because the Todo component is now calling useContext directly to access the context value. Any component can now access the color context value by calling useContext. The default value acts as a fallback. It’s the value that React will use when the consuming component, in this case Todo, is not wrapped in a Provider component. As such, the default value is currently static and cannot be changed from outside the context file.

Provider and Consumer

To allow child components to access the context values, you can wrap them in a component called the Provider component. This allows the children of this component to subscribe to the context value, so that when the context value changes, the children are automatically updated. The Provider component accepts a value prop that specifies the data you want to share with the consuming children.

App.js
import ColorContext from "./ColorContext"
import { useState } from "react"
import List from "./List"

function App() {
  const [done, setDone] = useState(false)
  const color = done ? "green" : "black"

  return (
    <>
      <h1 className="class">Tasks</h1>
      <label>
        <input type="checkbox" onChange={e => setDone(e.target.checked)} />
        Done
      </label>
      <hr />
      <ColorContext.Provider value={color}>
        <List done={done} />
      </ColorContext.Provider>
    </>
  )
}

Because we wrap the List component with the Provider component, any child of List can now access whatever value is in ColorContext.

The todos now toggle between green and black depending on the value of done, which is determined at the top-level App component. You can think of Provider as working similar to CSS inheritance. If you have a top-level div wrapped around other elements, those elements take on the color defined in the top-level div. If you change the div color, the elements inside it also change to match their parent div.

Side Note…

Earlier, we mentioned the Consumer component, which is the older way of consuming context. Instead of using useContext , the code of the consuming component would be like this:

Todo.js
...
function Todo({ todo, color, done }){
 const { color } = useContext(ColorContext)
 return (
  <ColorContext.Consumer>
  {(color) => (
 <p style={{ color }}>
   <label>
    <input type="checkbox" checked={done} readOnly={true} />
   </label>
   {todo.title} - {todo.owner}
  </p>
  )}
  </ColorContext.Consumer>
 )
}

You wrap the code with ColorContext.Consumer then use renderProps to ‘extract’ the values from the context provider. However, this is an old way of doing things and you should definitely use useContext.

Context API and Custom Hook

While it is possible to use useContext() in each place where the context value is needed, I prefer to call it once and use a custom hook in child components instead. In React, a custom hook is a function that encapsulates reusable logic related to state or other functionality. By defining a custom hook for accessing the context values, you can avoid repeating the same code in multiple places and make your code easier to read and maintain.

In the previous code, the done state is still being passed down to the Todo component through the List component. We can change this by using a custom hook to access the context value directly in the Todo component. This will allow us to avoid using prop-drilling and make the code cleaner and easier to maintain.

Step 1: Create an ‘Enhanced’ Provider component

In the ColorContext.js file, we will create a custom provider component called StatusContext. This component will handle the current state variables, as well as any other shared states that we may add in the future. Therefore, we will rename it from ColorContext to StatusContext to reflect its expanded role.

We will move the states and their corresponding logic that were previously declared in the App component to this new file. We will return them by calling the built-in Provider component and passing these states and handlers into the value prop.

Step 2: Wrap the necessary children with the provider

Next, we will wrap the necessary children with the provider component. In this case, the parent component App is also dependent on the context, as it holds the “done” checkbox. Therefore, we need to wrap the App component with the StatusProvider component. This will ensure that everything we returned in the StatusProvider component is available throughout the entire app, from the parent component down to the most deeply nested children.

src/index.js
import React from "react"
import ReactDOM from "react-dom/client"
import "./index.css"
import App from "./App"

import StatusProvider from "./StatusContext"

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
  <React.StrictMode>
    <StatusProvider>
      <App />
    </StatusProvider>
  </React.StrictMode>
)

At this point, you can read the context values by calling useContext within the children.

However, a better option is to use a custom hook for this purpose. To define a custom hook for accessing the context values, you can create a function that calls useContext() and returns the value you want to access.

hooks/useStatus.js
import { useContext } from "react"
import { StatusContext } from "../StatusContext.js"

const useStatus = () => {
  const context = useContext(StatusContext)
  if (!context)
    throw new Error("StatusContext must be used within StatusProvider")
  return context
}

export default useStatus

The custom hook also allows you to do some error handling. If in case you forget to wrap children with StatusProvider and then still call the useStatus hook from within any of the child components, you will get the following error in your console: useStatus error

Step 3: Use the custom hook

You can now use this custom hook in any child component that needs to access the context value, just like you would use any other hook. By wrapping the App component with StatusProvider, you can access the context value from anywhere within your React app. This can be particularly useful if your context values are needed throughout the entire app, such as authentication values like whether the user is logged in, their username, etc. The final state of our components:

App.js
import { useState } from "react"
import List from "./List"
import useStatus from "./hooks/useStatus"

function App() {
  const { handleDone } = useStatus()
  // ...
}

List no longer receives any props:

List.js
// ...
export default function List() {
  // ...

  return <ul>{todoItems}</ul>
}

Todo has direct access to its required props.

Todo.js
import useStatus from "./hooks/useStatus"

export default function Todo({ todo }) {
  const { done, color } = useStatus()
  return (
    <p style={{ color }}>
      <label>
        <input type="checkbox" checked={done} readOnly={true} />
      </label>
      {todo.title} - {todo.owner}
    </p>
  )
}

The useStatus hook returns an object that contains three values: done, color, and handleDone. To access these values, you can simply call the hook and destructure the properties that you need.

This will extract the values of done, color, and handleDone from the object returned by the hook and make them available for use in your component. You can then use these values as needed, such as by rendering them in the component’s template or passing them as props to other components.