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 Section
s.
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:
- You
createContext
for the data you are looking to share, passing a default value. - You
useContext
in each child component that wants to read that data. - You provide context via
Context.Provider
in a parent component, and set that context'svalue
. - 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 Heading
s 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 Button
s 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
andButton
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 whetherGreeting
orLoginForm
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-levelApp
component, but each<PlaceImage>
needs to be aware of it. App
passesimageSize
toList
, which passes it to eachPlace
, which passes it to thePlaceImage
. Remove theimageSize
prop, and instead pass it from theApp
component directly toPlaceImage
.
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:
- Create and export the context using the syntax
export const SomeContext = createContext(defaultValue)
. - Read the value of the context using the syntax
useContext(SomeContext)
, which can be done from any child component, no matter how deep. - 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.