Web Unit 3 Sprint 10 - Redux & State Management
Module 2: The Context API
In this module, you'll learn about React's Context API and how it can be used to share state between components without prop drilling. You'll explore how to create and consume context, and understand when to use it effectively in your applications.
Learning Objectives
- Describe what the Context API is and the problem it solves
- Provide data to the component tree with a Context Provider
- Consume data from a Context object in nested components
Understanding the Context API
In a typical React application, data is passed top-down (parent to child) via props, but this can be cumbersome for certain types of props that are required by many components within an application, like application state. Context provides a way to share data or state between components without having to explicitly pass a prop through every level of the tree.
We use the context API when we have global data that many components share or when we have to pass data through intermediate components.
How to Build It
The React docs give us a good explanation of what context is:
Usually, you will pass information from a parent component to a child component via props. But passing props can become verbose and inconvenient if you have to pass them through many components in the middle, or if many components in your app need the same information. Context lets the parent component make some information available to any component in the tree below it—no matter how deep—without passing it explicitly through props.
Because of one-way data flow in React, slices of application state ("is logged in", "todos", "is spinner on", etc) must exist above the components that need them.
If we try to keep state as close as possible to the components that need it we end up with a fragmented state, where the slices live in several components at different depths in the tree.
Context is commonly used as a way to centralize application state in such a way that every piece of state can be accessed readily from any component, no matter how deeply nested in the React tree, without going through the hassle of handing down props several levels.
Combining Context with other techniques like the Reducer pattern, we can build a framework that keeps application state both centralized and easily accessible by components. There are three aspects when working with Context:
- Creating a Provider that holds the data and makes it available.
- Wrapping a part of the component tree with the Provider.
- Consuming the state from any component that is a descendant of the Provider.
In the coming sections we will take a closer look at each.
Introduction to React Context API
Creating a Context Provider
Making data available through React Context is not hard, but it can feel a bit convoluted at first. In this objective we will cover two aspects of the process which have to do with creating the context and connecting it to the component tree, leaving for later actually consuming the data that is being made available:
- Creating a provider that holds the data and makes it available.
- Wrapping a part of the component tree with the provider.
How to Build It
To build a context provider, we will start by creating a new module for it, called bands.js
, inside a context
folder. The data that will be provided will just be some information about music bands:
const bands = {
metal: ['Metallica', 'Iron Maiden'],
}
Inside bands.js
, we will perform 4 distinct operations:
- Pull the needed imports at the top of the module.
- Declare the data you wish to make available.
- Create and export the context object.
- Create and export the context provider.
Study the following code. The steps above are noted in the comments:
// context/bands.js module
import React, { createContext } from 'react' // STEP 1 - imports
const bands = { // STEP 2 - declare the data you wish to share
metal: ['Metallica', 'Iron Maiden'],
}
// STEP 3 - export a context, mind the capitalization!
export const BandsContext = createContext()
// STEP 4 - export a context provider, mind the capitalization!
export const BandsProvider = ({ children }) => (
// the value prop holds the data
<BandsContext.Provider value={bands}>
{children}
</BandsContext.Provider>
)
There are two pieces being exported: the context object and the context provider. The context provider is pretty much a React component that renders its child elements. The value
prop you see in the code takes in the data we wish to make available.
Next, we will wrap the whole component tree using our provider (although you can wrap more localized parts of the component tree). Inside the index.js
module of the project:
// index.js module
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './components/App'
// here is the provider from the context/bands.js module
import { BandsProvider } from './context/bands'
const root = createRoot(document.getElementById('root'))
root.render(
// we could wrap just a part of the component tree
// but here we are wrapping the entire tree
<BandsProvider>
<App />
</BandsProvider>
)
If this looks similar to what you do when configuring React Router, that's because this library uses Context under the hood, as do many others. Typically you will only do the wrapping once per context and per application. You can have multiple contexts in the same application, which means that often, there are several levels of wrapping at the top of the app:
root.render(
<BandsProvider>
<FriendsProvider>
<AuthProvider>
<App />
</AuthProvider>
</FriendsProvider>
</BandsProvider>
)
In the next objective, you will learn how to consume the bands data from inside any component that is a descendant of the provider, no matter how deeply nested, and without any prop drilling!
Introduction to React Context API
Consuming Data From Context
Using the Context API is not hard, but it can feel a bit intimidating at first. In the last objective we learned how to make data available to any part of the component tree, using a context provider. In this objective we will look at consuming data from context, using the Context Hook>
How to Build It
This is how we left things in the last objective. We have some music bands data we wish to make available, we have a context object and a context provider, and we have wrapped our entire component tree with the provider:
// context/bands.js module
import React, { createContext } from 'react'
const bands = {
metal: ['Metallica', 'Iron Maiden'], // the DATA we are sharing
}
export const BandsContext = createContext()
export const BandsProvider = ({ children }) => (
<BandsContext.Provider value={bands}>
{children}
</BandsContext.Provider>
)
// index.js module
import { BandsProvider } from './context/bands'
const root = createRoot(document.getElementById('root'))
root.render(
<BandsProvider>
<App />
</BandsProvider>
)
The first step in this objective is to find a component that is interested in the bands data made available through context:
import React from 'react'
export default function DeeplyNestedComponent() {
return (
<div>
<h2>Hello, World!</h2>
<p>Fav band: {/* get Metallica from context */}</p>
<p>Next fav band: {/* get Iron Maiden from context */}</p>
</div>
)
}
The component above is deeply nested in the component tree and we don't wish to prop-drill. Luckily, the data we are after is provided via a bands Context!
We will require two imports: the Context hook and the bands context we created in the previous objective:
import React, { useContext } from 'react'
import { BandsContext } from '../context/bands'
Next, we will use the Context hook at the top of our component, passing it the context object exported from bands.js
:
import React, { useContext } from 'react'
import { BandsContext } from '../context/bands'
export default function DeeplyNestedComponent() {
const bandsData = useContext(BandsContext)
// the rest of the component...
}
If you console.log the bandsData
, you will find it maps exactly to the data we made available from our bands Context. Here is the finished code, using destructuring to access a particular property of the object shared via the bands context:
import React, { useContext } from 'react'
import { BandsContext } from '../context/bands'
export default function DeeplyNestedComponent() {
const { metal } = useContext(BandsContext)
return (
<div>
<h2>Hello, World!</h2>
<p>Fav band: {metal[0] /* renders Metallica */}</p>
<p>Next fav band: {metal[1] /* renders Iron Maiden */}</p>
</div>
)
}
And if "teleporting" music bands data deep down the component tree does not sound exciting enough, imagine we wished to share the entirety of application state using Context. These states can be quite large, so the Context API is the perfect mechanism for centralizing them and making them readily available. You will practice these moves in your Module Project!
Introduction to React Context API
Module Project
This project will have you refactor an existing app which currently uses its top-level component to hold application state. Your mission is to move this state to a context so it can be accessed directly by any component in the React tree without any penalty in the form of prop-drilling.
The Module Project contains advanced problems that will challenge and stretch your understanding of the module's content. The solution video is available below in case you need help or want to see how we solved each challenge (note: there is always more than one way to solve a problem). If you can successfully complete all the Module Projects in a sprint, you are ready for the Sprint Challenge and Assessment.
The link below will provide you with a copy of the Module Project on GitHub:
- Starter Repo: The Context API
- Fork and clone the code repository to your machine, and
- open the
README.md
file in VSCode, where you will find instructions on completing this Project. - Submit your GitHub url for the updated repository to the Sprint Challenge Submissions tab of your BloomTech portal for grading and review.
Watch if you get stuck or need help getting started.