As React applications grow, components are often organized into many levels.
A component near the top of the application may have information that a deeply nested component needs.
For example, imagine a Web4Learners website with this component structure:
App
└── Header
└── Navigation
└── UserMenu
└── ProfileButton
Suppose App knows the currently logged-in user, but ProfileButton needs that information.
Without Context, you might need to pass the user through every component:
App
↓ user
Header
↓ user
Navigation
↓ user
UserMenu
↓ user
ProfileButton
This is called prop drilling.
React Context provides a way for a component to make a value available to components deeper in the tree without manually passing that value through every intermediate component.
Context is useful for information that many components need, such as:
- Current theme
- Logged-in user
- Language preference
- Application settings
- Permissions
- Authentication information
- Shared application configuration
In this chapter, you will learn how useContext works, how to create Context, how providers and consumers work, how to use multiple contexts, how Context can be combined with useReducer, how Context affects performance, and when Context is or is not the right solution.
What Is React Context?
Context is a React feature that allows information to be shared with components throughout a component tree without passing that information through every intermediate component as props.
Normally, data flows from parent to child through props.
For example:
function App() {
const theme = "dark";
return <Header theme={theme} />;
}
Then:
function Header({ theme }) {
return <Navigation theme={theme} />;
}
Then:
function Navigation({ theme }) {
return <Button theme={theme} />;
}
This works, but every component between App and Button has to receive and forward the theme prop.
Context provides another approach.
App
↓
Context
↓
Header
↓
Navigation
↓
Button
The intermediate components do not need to receive the theme just to pass it along.
What Is useContext?
useContext is a React Hook that allows a component to read and subscribe to a Context value.
You import it from React:
import { useContext } from "react";
The basic syntax is:
const value = useContext(SomeContext);
For example:
const theme = useContext(ThemeContext);
The component receives the current value provided by the nearest matching Context provider above it in the component tree.
Creating Context
React provides createContext for creating a Context.
import { createContext } from "react";
const ThemeContext = createContext("light");
Here, ThemeContext represents the Context.
The value:
"light"
is the default value.
The default value is used when a component reads the Context without a matching provider above it.
Context Default Values
Consider:
const ThemeContext = createContext("light");
If a component uses:
const theme = useContext(ThemeContext);
and there is no provider above that component, theme will be:
light
The default value is useful as a fallback and can also make isolated component testing easier.
However, it is important to understand that the default value is not application state. It does not change when a provider’s value changes.
Creating a Theme Context
A common Context example is application themes.
import { createContext } from "react";
const ThemeContext = createContext("light");
Now the application has a Context that represents the current theme.
The next step is to provide a value.
Context Provider
A Provider makes a Context value available to components below it in the tree.
In modern React, you can use the Context itself as a provider:
<ThemeContext value="dark">
<App />
</ThemeContext>
This is the modern provider syntax.
You may also encounter the older, widely used syntax:
<ThemeContext.Provider value="dark">
<App />
</ThemeContext.Provider>
Both represent the same basic Context concept. The direct <ThemeContext value={...}> syntax is the modern React approach.
Providing a Context Value
For example:
import { createContext } from "react";
const ThemeContext = createContext("light");
function App() {
return (
<ThemeContext value="dark">
<Home />
</ThemeContext>
);
}
Every component inside Home can now read the Context.
Consuming Context
To consume a Context, use useContext.
import { useContext } from "react";
function Button() {
const theme = useContext(ThemeContext);
return (
<button className={theme}>
Click Me
</button>
);
}
The button does not need to receive theme as a prop.
React finds the nearest ThemeContext provider above Button and returns its value.
How Context Travels Through the Component Tree
Consider this structure:
App
│
└── ThemeContext
│
└── Header
│
└── Navigation
│
└── ProfileButton
If App provides:
<ThemeContext value="dark">
then ProfileButton can access:
const theme = useContext(ThemeContext);
The intermediate components do not need to pass the value manually.
This is one of the main benefits of Context.
A Complete Context Example
Here is a simple theme application:
import { createContext, useContext } from "react";
const ThemeContext = createContext("light");
function App() {
return (
<ThemeContext value="dark">
<Page />
</ThemeContext>
);
}
function Page() {
return <Header />;
}
function Header() {
return <ThemeButton />;
}
function ThemeButton() {
const theme = useContext(ThemeContext);
return (
<button className={theme}>
Current theme: {theme}
</button>
);
}
export default App;
Notice that Page and Header do not need to receive or forward theme.
Only the component that actually needs the value consumes it.
Context With State
Context becomes more useful when its value comes from state.
For example:
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext(null);
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext value={{ theme, setTheme }}>
<Page />
</ThemeContext>
);
}
A child component can then read both the value and the function:
function ThemeButton() {
const { theme, setTheme } = useContext(ThemeContext);
function toggleTheme() {
setTheme((currentTheme) =>
currentTheme === "light" ? "dark" : "light"
);
}
return (
<button onClick={toggleTheme}>
Current theme: {theme}
</button>
);
}
Now Context provides both:
theme
setTheme
This allows components deeper in the tree to read and update shared state.
Creating a Context Provider Component
As applications grow, it is often cleaner to create a dedicated provider component.
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext(null);
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext value={{ theme, setTheme }}>
{children}
</ThemeContext>
);
}
The application can then use:
function App() {
return (
<ThemeProvider>
<Home />
</ThemeProvider>
);
}
Now all components inside Home can access the theme.
Why Create a Provider Component?
A dedicated provider component gives you a place to keep:
- Shared state
- State update functions
- Context logic
- Helper functions
- Effects related to that shared state
- Derived values
Instead of putting everything directly inside App, you can organize the application into focused providers.
Building a Custom Context Hook
A useful pattern is creating a custom Hook for consuming Context.
Instead of writing:
const { theme, setTheme } = useContext(ThemeContext);
throughout the application, you can create:
function useTheme() {
return useContext(ThemeContext);
}
Then components can use:
const { theme, setTheme } = useTheme();
This can make the Context API easier to use throughout a project.
Handling Missing Providers
A Context can be created with null as its default:
const ThemeContext = createContext(null);
Then:
function useTheme() {
const context = useContext(ThemeContext);
if (context === null) {
throw new Error("useTheme must be used inside ThemeProvider");
}
return context;
}
Now developers get a clear error if they accidentally use the Hook outside the provider.
This is especially useful in larger applications.
Multiple Contexts
An application can have more than one Context.
For example:
const ThemeContext = createContext(null);
const UserContext = createContext(null);
const LanguageContext = createContext(null);
You can provide all three:
function App() {
return (
<ThemeProvider>
<UserProvider>
<LanguageProvider>
<Home />
</LanguageProvider>
</UserProvider>
</ThemeProvider>
);
}
A component can consume multiple contexts:
function Profile() {
const { theme } = useTheme();
const { user } = useUser();
const { language } = useLanguage();
return (
<div className={theme}>
<h1>{user.name}</h1>
<p>Language: {language}</p>
</div>
);
}
Each Context handles a different concern.
Why Separate Contexts?
It is usually better to keep unrelated data separate.
For example, instead of one enormous Context:
<AppContext
value={{
theme,
user,
language,
cart,
notifications,
settings,
}}
>
you might have:
ThemeContext
UserContext
LanguageContext
CartContext
NotificationContext
This makes the application easier to understand and can also reduce unnecessary subscriptions to unrelated Context values.
Context + Reducer
Context can be combined with useReducer to build a shared state system.
This is particularly useful when:
- Many components need the same state
- Many components need to update that state
- State transitions are complex
- You want centralized update logic
The basic architecture is:
Context
↓
Provider
↓
useReducer
↓
Shared State + Dispatch
↓
Child Components
Creating a Reducer
Suppose Web4Learners has a shopping cart.
function cartReducer(state, action) {
switch (action.type) {
case "add":
return [
...state,
action.product,
];
case "remove":
return state.filter(
(product) => product.id !== action.id
);
default:
return state;
}
}
The reducer describes how the cart changes in response to actions.
Creating Cart Contexts
You can create separate contexts for state and dispatch:
const CartStateContext = createContext(null);
const CartDispatchContext = createContext(null);
Then create a provider:
function CartProvider({ children }) {
const [cart, dispatch] = useReducer(cartReducer, []);
return (
<CartStateContext value={cart}>
<CartDispatchContext value={dispatch}>
{children}
</CartDispatchContext>
</CartStateContext>
);
}
Now components can access either the cart state or the dispatch function.
Consuming Context + Reducer
A cart display might use:
function Cart() {
const cart = useContext(CartStateContext);
return (
<div>
<h2>Shopping Cart</h2>
{cart.map((product) => (
<p key={product.id}>
{product.name}
</p>
))}
</div>
);
}
A button can access dispatch:
function AddToCartButton({ product }) {
const dispatch = useContext(CartDispatchContext);
return (
<button
onClick={() =>
dispatch({
type: "add",
product,
})
}
>
Add to Cart
</button>
);
}
This creates a clean separation between:
State
and
Actions
Why Context + Reducer Is Powerful
useReducer handles how state changes.
Context handles how that state is shared.
Together they can provide a centralized state architecture without passing props through many layers.
For example:
CartProvider
│
├── cart state
├── dispatch
│
├── ProductList
│ └── AddToCartButton
│
└── CartSummary
The deeply nested components can access the shared cart state and dispatch actions without prop drilling.
A Complete Context + Reducer Example
import {
createContext,
useContext,
useReducer,
} from "react";
const CartStateContext = createContext(null);
const CartDispatchContext = createContext(null);
function cartReducer(state, action) {
switch (action.type) {
case "add":
return [...state, action.product];
case "remove":
return state.filter(
(product) => product.id !== action.id
);
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function CartProvider({ children }) {
const [cart, dispatch] = useReducer(cartReducer, []);
return (
<CartStateContext value={cart}>
<CartDispatchContext value={dispatch}>
{children}
</CartDispatchContext>
</CartStateContext>
);
}
function CartSummary() {
const cart = useContext(CartStateContext);
return <p>Items in cart: {cart.length}</p>;
}
function AddToCartButton({ product }) {
const dispatch = useContext(CartDispatchContext);
return (
<button
onClick={() =>
dispatch({
type: "add",
product,
})
}
>
Add to Cart
</button>
);
}
This pattern is useful for medium-sized applications where several components need access to the same state.
Context Performance
Context is convenient, but it is important to understand its performance behavior.
When a provider receives a different value, components that consume that Context can re-render.
Consider:
<ThemeContext value={{ theme, setTheme }}>
<App />
</ThemeContext>
The object:
{ theme, setTheme }
is created again during each render of the provider.
That means its identity can change even when the individual values have not meaningfully changed.
In some applications, this can cause unnecessary re-renders of Context consumers.
Stabilizing Context Values
For more complex applications, you can use useMemo when appropriate.
const value = useMemo(
() => ({ theme, setTheme }),
[theme]
); return ( <ThemeContext value={value}> {children} </ThemeContext> );
Now the object is reused while theme remains the same.
However, useMemo should not be added automatically. First understand where unnecessary renders actually occur and measure when performance matters.
Splitting Contexts for Performance
Suppose you have:
const AppContext = createContext(null);
and its value contains:
{
user,
theme,
notifications,
cart
}
A component that only needs the theme is still subscribed to the entire Context value.
Separating concerns can help:
UserContext
ThemeContext
NotificationContext
CartContext
Now a component can subscribe only to the Context it needs.
Separate State and Dispatch Contexts
The Context + Reducer pattern can also separate state and dispatch.
const StateContext = createContext(null);
const DispatchContext = createContext(null);
A component that only needs to dispatch actions can consume the dispatch Context without consuming the state Context.
This can be useful in larger applications.
Context Does Not Automatically Solve All State Management
Context is often described as a state management solution, but Context itself does not manage state.
Context provides a way to share values.
For example:
<ThemeContext value="dark">
Context is simply distributing the value.
If you want the value to change, you need something such as:
useState
or:
useReducer
So a common architecture is:
useState/useReducer
↓
Context
↓
Components
Context vs Props
Props and Context solve different problems.
Props
Props are ideal when data needs to move directly from a parent to a child.
<UserProfile user={user} />
Props make the data relationship explicit.
Context
Context is useful when many components at different levels need the same value.
const user = useContext(UserContext);
There is no need to manually forward the value through intermediate components.
Context vs State
State answers:
Where is changing data stored?
Context answers:
How can a value be made available to components deeper in the tree?
They are often used together.
For example:
const [theme, setTheme] = useState("light");
stores the state.
Then:
<ThemeContext value={{ theme, setTheme }}>
shares it.
When to Use Context
Context is particularly useful when a value is needed by many components at different levels of the component tree.
Common examples include:
Theme
light
dark
Many components may need to know the current theme.
Authentication Information
For example:
current user
login status
permissions
Many parts of the application may need access to this information.
Language
A multilingual application may need a current language throughout many components.
Application Settings
Global preferences can sometimes be shared through Context.
Shared UI Configuration
Examples include:
- Modal configuration
- Toast notifications
- Accessibility preferences
- Design system settings
When Not to Use Context
Context is not automatically better than props.
If only one child needs a value, props are usually simpler.
For example:
function App() {
const username = "Karim";
return <Profile username={username} />;
}
There is no reason to create a Context just for this simple parent-child relationship.
Don’t Use Context for Every Piece of State
Suppose a form contains:
const [email, setEmail] = useState("");
If only the form component needs email, keep it local.
You do not need:
EmailContext
just because Context exists.
Local state is often the simplest solution.
Context and Component Design
Before creating a Context, ask:
How many components actually need this value?
If the answer is one or two nearby components, props may be clearer.
If the value is needed by many components across different levels, Context may be useful.
This keeps the application architecture easier to understand.
Context and Custom Hooks
A common modern pattern is:
Context
↓
Provider
↓
Custom Hook
↓
Components
For example:
const UserContext = createContext(null);
function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
<UserContext value={{ user, setUser }}>
{children}
</UserContext>
);
}
function useUser() {
const context = useContext(UserContext);
if (context === null) {
throw new Error(
"useUser must be used inside UserProvider"
);
}
return context;
}
A component can now simply write:
function Profile() {
const { user } = useUser();
return <h1>{user.name}</h1>;
}
This hides the Context implementation details from most components.
Context Provider Placement
A provider should generally be placed at the lowest level of the tree that needs to share the value.
For example, if only a section of the application needs a particular Context, there is no need to wrap the entire application.
Instead of:
App
└── ThemeProvider
└── Everything
you might have:
App
├── PublicPages
│
└── Dashboard
└── DashboardProvider
└── Dashboard Components
This keeps the Context’s scope intentional.
Nested Providers
You can also have multiple providers for the same Context.
For example:
<ThemeContext value="dark">
<Header />
<ThemeContext value="light">
<Documentation />
</ThemeContext>
</ThemeContext>
Components inside Documentation receive:
light
because the nearest provider takes precedence.
Components outside the nested provider but inside the outer provider receive:
dark
This can be useful when different sections of an application need different Context values.
Context and Component Reuse
Context can sometimes make components less reusable if they depend directly on a specific Context.
For example:
function UserAvatar() {
const { user } = useUser();
return <img src={user.avatar} alt={user.name} />;
}
This component now expects the UserContext to exist.
An alternative is to pass the user as a prop:
function UserAvatar({ user }) {
return <img src={user.avatar} alt={user.name} />;
}
This can make the component easier to reuse in different environments.
There is no universal rule. Choose the approach that best represents the component’s responsibility.
Context Best Practices
Keep Context Focused
Prefer contexts with a clear purpose.
For example:
ThemeContext
UserContext
CartContext
are easier to understand than one giant context containing every application value.
Keep Local State Local
Do not move every piece of state into Context.
State should generally live as close as possible to the components that need it.
Use Props When the Data Path Is Simple
Props are explicit and often easier to understand for straightforward parent-child communication.
Use Custom Hooks
A custom Hook can provide a clean API around your Context.
For example:
const { user, login, logout } = useUser();
is easier to use throughout an application than repeatedly accessing the raw Context object.
Avoid Unnecessary Provider Nesting
Providers are useful, but dozens of nested providers can make an application harder to understand.
Group related providers when appropriate.
Think About Performance
Large, frequently changing Context values can cause many consumers to re-render.
Consider splitting contexts or structuring the provider carefully when performance becomes important.
Common Context Mistakes
Mistake: Using Context for Everything
Context is not a replacement for all props or all local state.
Use the simplest solution that fits the data flow.
Mistake: Creating One Giant Context
Avoid putting unrelated application state into one Context.
Instead, separate different concerns.
Mistake: Forgetting the Provider
If a component expects a Context value but is rendered outside the provider, it receives the Context’s default value.
Using null as the default and throwing a clear error in a custom Hook can help identify this problem.
Mistake: Mutating Context State Directly
If Context provides an array or object managed by state, update it through the appropriate state setter or reducer.
Do not mutate the existing value directly.
Mistake: Ignoring Context Performance
If a Context value changes frequently and many components consume it, those consumers may re-render.
Consider whether the Context should be split or whether the state belongs closer to the components that use it.
Mistake: Assuming Context Is a Database
Context only makes a value available through the component tree.
It does not automatically persist data, fetch data, synchronize with a server, or provide database functionality.
Context Architecture Example
A larger Web4Learners application might use:
App
│
├── ThemeProvider
│ └── ThemeContext
│
├── UserProvider
│ └── UserContext
│
└── CartProvider
├── CartStateContext
└── CartDispatchContext
Components can then access only the information they need.
For example:
function Header() {
const { user } = useUser();
const { theme } = useTheme();
return (
<header className={theme}>
<span>{user.name}</span>
</header>
);
}
And a product page can use the cart:
function ProductCard({ product }) {
const dispatch = useCartDispatch();
return (
<article>
<h2>{product.name}</h2>
<button
onClick={() =>
dispatch({
type: "add",
product,
})
}
>
Add to Cart
</button>
</article>
);
}
This architecture keeps different concerns separate.
A Simple Mental Model for Context
Think of Context as a value placed into a part of the component tree.
Provider
↓
Shared value
↓
────────────────────
│ │ │
Component Component Component
↓
useContext
Any descendant component can read the value.
The component does not need to receive that value through props from every intermediate component.
Props vs Context
| Feature | Props | Context |
|---|---|---|
| Data flow | Parent to child | Provider to descendants |
| Best for | Direct relationships | Shared values |
| Explicit | Very explicit | Less explicit |
| Avoids prop drilling | No | Yes |
| Requires provider | No | Usually |
| Good for local state | Yes | Usually unnecessary |
| Good for app-wide settings | Possible | Often useful |
| Good for deeply shared data | Can become cumbersome | Useful |
Neither approach is universally better.
The right choice depends on the component structure and how widely the data is needed.
Context + Reducer Cheat Sheet
const StateContext = createContext(null);
const DispatchContext = createContext(null);
Create reducer:
function reducer(state, action) {
switch (action.type) {
case "add":
return [...state, action.item];
default:
return state;
}
}
Create provider:
function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, []);
return (
<StateContext value={state}>
<DispatchContext value={dispatch}>
{children}
</DispatchContext>
</StateContext>
);
}
Consume state:
const state = useContext(StateContext);
Consume dispatch:
const dispatch = useContext(DispatchContext);
Dispatch an action:
dispatch({
type: "add",
item,
});
Practice Exercise
Create a Web4Learners Theme System.
Your application should:
- Create a
ThemeContext - Create a
ThemeProvider - Store the theme using
useState - Provide
themeandsetTheme - Create a custom
useThemeHook - Display the current theme
- Add a button to switch between light and dark modes
- Allow deeply nested components to access the theme without prop drilling
Start with:
import {
createContext,
useContext,
useState,
} from "react";
const ThemeContext = createContext(null);
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext value={{ theme, setTheme }}>
{children}
</ThemeContext>
);
}
function useTheme() {
const context = useContext(ThemeContext);
if (context === null) {
throw new Error(
"useTheme must be used inside ThemeProvider"
);
}
return context;
}
Then create a component that consumes the Context:
function ThemeButton() {
const { theme, setTheme } = useTheme();
return (
<button
onClick={() =>
setTheme((current) =>
current === "light" ? "dark" : "light"
)
}
>
Current theme: {theme}
</button>
);
}
Key Takeaways
- Context allows values to be shared with components deep in the component tree.
createContextcreates a Context.useContextreads and subscribes to a Context value.- A provider makes a value available to descendant components.
- Modern React supports using the Context itself as the provider:
<SomeContext value={...}>. - The older
<SomeContext.Provider value={...}>syntax is also widely used. - A Context’s default value is used when there is no matching provider above the consumer.
- Context helps solve prop drilling.
- Context does not replace local state.
- Context and state are often used together.
- Multiple contexts can be used for different concerns.
- Context can be combined with
useReducerfor shared state with centralized state transitions. - Reducers describe how shared state changes.
- Dispatch sends actions to the reducer.
- Splitting Contexts can improve separation of concerns and may help reduce unnecessary re-renders.
- Frequently changing Context values can cause consuming components to re-render.
useMemocan sometimes help stabilize a provider value, but it should be used when there is a real reason.- Keep local state local when only a small part of the application needs it.
- Props are often simpler for direct parent-to-child communication.
- Context is useful when the same value is needed by many components at different levels.
- Custom Hooks provide a clean way to expose Context functionality.
- Providers should generally be scoped to the part of the tree that needs them.
- Context is a sharing mechanism, not a database or automatic server-state solution.
The most important idea to remember is:
Context helps components access shared values without passing those values through every intermediate component.