320H.10 - React Hooks: useContext

Learning Objectives

By the end of this lesson, learners will be able to:

  • Describe the purpose of React context.
  • Avoid prop drilling through the use of React context.
  • Use createContext to define and export new context for an application.
  • Use the useContext hook to read from context within descendant components.
  • Use the Context.Provider component to provide context from a parent component.
  • Integrate context with state to allow for interactive context values.
  • Explain alternatives to context.
  • Explain common use cases for context.

 CodeSandbox

This lesson uses CodeSandbox as one of its tools.

If you are unfamiliar with CodeSandbox, or need a refresher, please visit our reference page on CodeSandbox for instructions on:

  • Creating an Account
  • Making a Sandbox
  • Navigating your Sandbox
  • Submitting a link to your Sandbox to Canvas

The Problem with Props

One of the largest pain points for React development is prop drilling - sending props down the component tree from parent to child to grandchild and on. When component heirarchies become very deeply nested, this becomes even more difficult to manage. "Lifting state" into the nearest common ancestor can be quite daunting, and passing that state through many components that don't need it is counterintuitive.


Prop Drilling




When passing props becomes overly verbose and inconvenient, either from passing them through many intermediary components or because many component within the application need the same information, we look for the solution in context. Context effectively allows us to "teleport" data to components within the tree without passing props!


The useContext Hook

Context lets a parent component make some piece of information available to any component in the tree below it, no matter how many children deep, without passing that information explicitly through props.

Let's look at an example from the React documentation which demonstrates a simple use case for context, then we'll discuss how the useContext hook works.

This Heading component accepts a prop level to determine its font size:

Now imagine you're building an application with multiple sections, and each section should have the same heading font sizes:

The level prop is currently being passed to each Heading separately, but it would be much more convenient to pass level to the Section components instead. This would also enforce our intention to have each Heading within a Section have the same font size.

<Section level={3}>
  <Heading>About</Heading>
  <Heading>Photos</Heading>
  <Heading>Videos</Heading>
</Section>

In order for the Heading components to know information about their parent, they would need to be passed props. This one-way data flow is intentional, and blocks child components from "asking" things about their parents or requesting information. In short, the solution above cannot be accomplished with props alone.

This is where useContext saves the day. Context allows a parent, even a distant parent, to provide data to its entire internal component tree without the use of props, sending that data directly where it needs to go.


Using Context




In order to use context, we must first create it using createContext. Then, we will useContext inside of the child components, and finally provide the context from the component that specifies the data using context provider components.


Creating Context

In order to create context, we traditionally export it from its own file so that many components can import it.

In the case of our example, we're creating a context for the "level" property, so we'll call it LevelContext and put it in a file of the same name - LevelContext.js:

import { createContext } from 'react';

export const LevelContext = createContext(1);

createContext takes the default value as its only argument. In this case we've used 1, which is the largest heading size. It is very important to note that you can pass any kind of value, even objects, to createContext. We will talk more about how to make use of this flexibility soon.


Using Context

In order to use context, we need to import both the useContext hook and whatever context we would like to use (in our case LevelContext):

import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

The Heading component from our example currently looks like this, taking the level from its props:

export default function Heading({ level, children }) {
  // ...
}

We're going to remove the level from props, and instead read its value from the LevelContext we imported above using useContext.

The useContext hook takes a single argument: the context to use.

export default function Heading({ children }) {
  const level = useContext(LevelContext);
  // ...
}

Just like any other React hooks, you can only call useContext inside of a React component at the top level (outside of loops or conditions). useContext simply tells React that our component would like to read from the context we've provided to it.

React automatically re-renders components that read some context if it changes.

If we update the example application's markup, here's where we're currently at:

Hm. All of the headings are the same size...

Even though we've created and used the context, we haven't provided it yet. React does not automatically know where to get the context from!

Think about this: we want each Section component to have its own level value, but we've imported all of our LevelContext from one file (that gives the value of 1 by default). Nowhere in our Section component do we modify this value. If we were to modify the value within Section, would it not just modify all instances of useContext?

We fix this issue by having each Section component provide its own context through the use of Context.Provider.


Providing Context

Here's what the example Section component currently looks like:

export default function Section({ children }) {
  return (
    <section className="section">
      {children}
    </section>
  );
}

In order to provide a unique context for each section, we use the Context.Provider component given to us by createContext. The Context.Provider component accepts a value prop, which is the value that all child components will read from the context.

Since we have already created the LevelContext, we simply need to import it into our Section component and wrap its children in a LevelContext.Provider and set its value to the level prop that Section is receiving:

import { LevelContext } from './LevelContext.js';

export default function Section({ level, children }) {
  return (
    <section className="section">
      <LevelContext.Provider value={level}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}

It is important to note that useContext will read its value from the nearest context provider within the component tree. Thanks to this behavior, we can nest our new Section component without worrying about the Headings within them taking the LevelContext value from one of the higher Sections.

Here's how the completed example looks!

This accomplishes the original goal, and provides the same result as passing individual level props to each Heading without requiring we do so. We've effectively allowed each Heading to "ask" for its level from the nearest Section above it.

Here's a quick review of our understanding of context so far:

  1. You createContext for the data you are looking to share, passing a default value.
  2. You useContext in each child component that wants to read that data.
  3. You provide context via Context.Provider in a parent component, and set that context's value.
  4. The children read the context from their closest parent provider.

Nested Context

While the example above works, it is not entirely efficient for the goal it achieves.

You can use and provide context from the same component, which can lead to creative and convenient solutions to common problems or inconveniences.

For example, currently, we're setting each section's level manually:

export default function Page() {
  return (
    <Section level={1}>
      ...
      <Section level={2}>
        ...
        <Section level={3}>
          ...

However, since context reads from the closest provider above it, each Section component can useContext from the Section above! Using this knowledge, we can start with our default LevelContext value - 1, and increment it on its way down the "context tree."

Here's how that would look within the Section component:

import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Section({ children }) {
  const level = useContext(LevelContext);
  return (
    <section className="section">
      <LevelContext.Provider value={level + 1}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}

Now, each Section reads level from the Section above it, and passes level + 1 down to all of its Heading children and the next Section. This process repeats indefinitely, allowing us to nest as many Section components as we want without ever having to worry about passing them the correct level prop.

A small caveat: useContext in a component is not affected by providers returned from the same component. The corresponding Context.Provider needs to be above the component doing the useContext() call.

Here's a completed example of this behavior in action:

Just like that, we've gone from providing level individually as props to each Heading component, to not having to manually provide it at all!


Intermediate Components

While the example so far has provided context to immediate children, it is very important to understand that context is provided the the entire component tree beneath it, no matter how deeply nested the components are. This is the major selling point of context.

For example, even if we nested a Heading component within many arbitrary <div> components, it would still read LevelContext from its nearest Section ancestor:

<Section>
  <div>
    <div>
      <div>
        <div>
          <div>
            <Heading>Reading context from nearest Section</Heading>
          </div>
        </div>
      </div>
    </div>
  </div>
</Section>

This behavior applies to built-in components like <div> and any components we create ourselves.

Here's an extended example that includes a new component, Post, which renders with a dashed red border for visibility. Post contains a Heading component, but no matter where Post is rendered, its nested Headings read their context from the nearest Section.

We did not need to do any additional management of heading sizes or any additional context wiring for this to work; it is already an automatic function of the way we created Section and Heading. In this way, we've created a very scalable system with these two components.


Context in Context

A good comparison to understand context is CSS inheritance. When you set a property, like color: red, on a DOM node, any node inside of it inherits that color, no matter how deep, unless another node in the middle overrides it. Context behaves in the same way. If we set a context, like value="red" on a component, any component inside of it inherits that context, no matter how deep, unless another node in the middle overrides it.

Similary, just like different CSS properties don't override each other, different contexts don't override each other. Every context that is created with createContext is unique and independant. Just like DOM nodes can have many different CSS attributes, React components can use and provide many different contexts at once.

You will understand how this can be such a powerful technique when we talk about more real-world use cases for context shortly.

First, we need to talk about when not to use context.


Alternatives to Context

Context is a powerful and convenient tool, and like other powerful and convenient tools in programming, it is easy to overuse. Until you have significant experience in determining when and when not to use context, it is best to follow a process to make that determination for you.

When building your applications, start by passing props. Even if you need to pass props down several levels into the component tree, it doesn't mean you need to use context. It is not unheard of to pass tens of props down through tens of components.

Looking at development from a team perspective, making data flow explicit with props gives a clear indication of what is happening. It may feel like a chore, but the developers that end up maintaining that code will be grateful for the clarity.

Think about our previous Section/Heading example - if you looked at it at a glance, would you know why or how those headings were becoming smaller? Where would you begin looking? The CSS? The Heading component? How long would it take you to realize how it worked and make changes?

For such a simple application, it may not take long, but as applications scale to thousands of lines of code and dozens of files...

This is why it is important to stay organized, keep your code clean, concise, and clear, and document it well. Remember, even if you aren't developing as part of a team, you're still developing as part of a team. Future you is a different person, and they will look back at your current good habits with appreciation!

Does this mean that we shouldn't use context? No! Like most tools, there's a place and a time.

If you end up passing props through many layers of intermediate components that don't use the data (and only pass it further down the tree), it is possible you forgot to extract some components along the way.

For example, if we have a simple blog application that has a Layout component that contains a Posts component, our final application export could look something like this:

export default function Blog() {
  const posts = []; // some list of posts data

  return (
    <>
      <Navigation />
      <Layout posts={posts} />
      <Footer />
    </>
  );
}

Since Layout itself doesn't need the posts data, we could have it take children as a prop and instead do this:

export default function Blog() {
  const posts = []; // some list of posts data

  return (
    <>
      <Navigation />
      <Layout>
        <Posts posts={posts}>
      </Layout>
      <Footer />
    </>
  );
}

Extracting components where possible allows us to reduce the number of layers between the component that contains the data and the one that needs it.

If neither props nor component extraction works well your your particular use case, that is when to consider using context.


Context Use Cases: Themes

Modern React development makes use of context in a number of ways. In this lesson, we'll briefly touch on some of the most common use cases, but be creative within your own projects!

Themes: If your application allows the use of different themes like light mode or dark mode, it is very common to place a context provider at the top of an application to provide a ThemeContext to the entire application.

Let's look at another example from the React documentation. Here, we have a page MyApp component that contains a Form component with many Button components. The Form is wrapped in a ThemeContext.Provider which provides the value dark to the child components. Within the Button components, we can then useContext to receive this value, regardless of how deeply nested the Buttons are.

function MyApp() {
  return (
    <ThemeContext.Provider value="dark">
      <Form />
    </ThemeContext.Provider>
  );
}

function Form() {
  // ... renders buttons inside ...
}
import { useContext } from 'react';

function Button() {
  const theme = useContext(ThemeContext);
  // ...

A working example of this is included below.

  • Try changing the theme value from "dark" to "light" in the MyApp component.
  • Explore how this is applied to both the Panel and Button components, and how the CSS is handled.
  • Create a new theme color within the CSS, and update the ThemeContext.Provider to supply this value.

While this works, it is not perfect. Currently, we have no way for the user to change the theme! We're doing it manually within the code itself, which is not how a web application should work. However, there is a very convenient solution...

You can pass state with context.

Remember how we mentioned earlier that you can set any value to context, even objects? That includes state, state objects, state arrays, pieces of state, etc. This is very important, so we'll say it one more time:

You can pass state with context.

So let's update the values passed via context by combining it with the useState hook. To do so, declare a state variable within the parent component hosting the Provider, and pass the state down as the context value, like so:

function MyPage() {
  const [theme, setTheme] = useState('dark');
  return (
    <ThemeContext.Provider value={theme}>
      <Form />
    </ThemeContext.Provider>
  );
}

Since state and context are linked to the rendering of components, any time the state changes, the new value will be passed to the child components, which will be immediately re-rendered.

What if we wanted to easily change to light theme? We already have a Button component that we can reuse, and now that we've tied the theme into state, creating this feature becomes as simple as:

function MyPage() {
  const [theme, setTheme] = useState('dark');
  return (
    <ThemeContext.Provider value={theme}>
      <Form />
      <Button onClick={() => {
        setTheme('light');
      }}>
        Switch to light theme
      </Button>
    </ThemeContext.Provider>
  );
}

That, however, doesn't quite work. Since Button is a custom component, not a built-in React element like <button>, onClick here is a prop. This means we need to edit our Button component to accept this prop and put it to work:

function Button({ onClick, children }) {
  const theme = useContext(ThemeContext);
  const className = "button button-" + theme;
  return (
    <button className={className} onClick={onClick}>
      {children}
    </button>
  );
}

Now, we have a working switch!

This comes with an obvious issue: we should be able to toggle between the two themes easily. Right now, we would have to refresh the page to go back to dark theme. We could add a second button to switch to dark theme, but it is more convenient to add a single toggle between the two.

Now we have a working theme! This is one of the most common use cases for context, though far from the only one. In fact, it is very common to have multiple context providers supplying information to components at once.

Let's take a few minutes to explore the extended example below, which now includes a piece of state called currentUser which is being supplied via the CurrentUserContext.Provider.

  • Notice how we have nested the two context providers within one another.
  • Also take note of the construction of the WelcomePanel component, which uses a ternary operator to determine whether Greeting or LoginForm should rendered within it.

As applications scale, this nested context structure can lead to a very deep pyramid of provider components. In order to avoid this, you can move all of your context providers at a certain level of the component tree into a seperate, single component. For example, we could refactor the code above to include this component MyProviders:

function MyProviders({ children, theme, setTheme }) {
  const [currentUser, setCurrentUser] = useState(null);
  return (
    <ThemeContext.Provider value={theme}>
      <CurrentUserContext.Provider
        value={{
          currentUser,
          setCurrentUser
        }}
      >
        {children}
      </CurrentUserContext.Provider>
    </ThemeContext.Provider>
  );
}

Since the MyApp component still requires the theme state, we leave that piece of state there and pass it through props to the MyProviders component.

Give this a try! In the CodeSandbox provided above, remove the current context providers and replace them with the MyProviders component. Be sure to set any relevant props!

As apps get even larger, it is very common to move context out of component files and hide the "wiring" between state and components. As you may have realized, this is very similar to the purpose of reducer functions and the useReducer hook!

Combining context and reducers is one of the way we can scale React applications, and it is a topic we will cover in the next lesson.


Other Context Use Cases

Some of the other common use cases for context include:

  • Current User/Account: Many components, often the entire application, will need to know what user is currently logged into the application. As shown in the example above, putting that information into context can make it convenient to find anywhere within the component tree. Since some applications allow you to have multiple accounts simultaneously (such as leaving a comment under a different name), they often wrap a part of the component tree in a nested context provider with the other account values.
  • Routing: Most pre-packaged routing libraries and solutions use context under the hood to hold the current route information. This is how different links know whether they're active or not.
  • Managing State: Complex apps often scale to have a lot of state information, and a lot of it ends up near the top of the application's component heirarchy. This is where it becomes common to use reducers alongside context to manage complex state and pass it down to distant components.

It is important to remember that context is not limited to static values. You can modify context values with a variety of techniques including state and reducers, and React will automatically update all components that read an updated context value during its next render cycle. This is why context is often paired with state.


Practice Activity: Eliminate Prop Drilling

The following sandbox contains another bit of example code from the React documentation. For the sandbox, and replace the existing prop drilling with context.

  • Toggling the checkbox changes the imageSize prop passed to each <PlaceImage>. The checkbox state is held in the top-level App component, but each <PlaceImage> needs to be aware of it.
  • App passes imageSize to List, which passes it to each Place, which passes it to the PlaceImage. Remove the imageSize prop, and instead pass it from the App component directly to PlaceImage.

You can declare context in Context.js.


Summary: Context

Context is a convenient way to provide data from a component to its entire child component tree.

In order to use context, you must:

  1. Create and export the context using the syntax export const SomeContext = createContext(defaultValue).
  2. Read the value of the context using the syntax useContext(SomeContext), which can be done from any child component, no matter how deep.
  3. Wrap the child components within a context provider and provide a value using the syntax <SomeContext.Provider value={someValue}> from within the parent component that sets the context value.

Context will pass through all intermediate components, allowing you to avoid prop drilling.

One of the major benefits of context is giving components the ability to "adapt to their surroundings" by reading values from their ancestors.

Context is not always the recommended solution; try using props or passing components in JSX as children before you resort to using context.

Copyright © Per Scholas 2024