In the lifting state lesson we met prop drilling: passing data through many components that do not use it. Context is React's built-in way to make some data available to any component below a certain point, without passing props through every level. Think of it as a shared board in a building that every room can read.
When is context a good choice?
Use context for data that many components at different levels need, and that does not change very often. Typical examples:
- The current user who is signed in.
- The theme: light or dark.
- The chosen language.
- A shopping cart used by the header, the product pages and the checkout.
Do not use context for everything. If only a parent and child need a value, props are simpler and clearer.
The three steps
- Create the context with
createContext(defaultValue). - Provide a value with
<Context.Provider value={...}>around the part of the page that needs it. - Use the value in any component below with
useContext(Context).
Example: a theme
First we create the context, in its own file:
// ThemeContext.jsx
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext('light');
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
function toggleTheme() {
setTheme(theme === 'light' ? 'dark' : 'light');
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
ThemeProvider is a normal component that keeps the theme in state and shares it. children is whatever you put inside it. We also export a small helper, useTheme, so other components do not need to know about useContext or the context object. It is a custom hook, which lesson 17 explains.
Next we wrap the app once, near the top:
// main.jsx
createRoot(document.getElementById('root')).render(
<ThemeProvider>
<App />
</ThemeProvider>
);
Now any component, at any depth, can read the theme:
function ThemeButton() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
);
}
function Page() {
const { theme } = useTheme();
return <main className={theme}>Hello!</main>;
}
No props travel through the layers between. When toggleTheme runs, the provider's state changes, and every component that uses the context draws again with the new value.
Example: the signed-in user
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
function login(name) { setUser({ name }); }
function logout() { setUser(null); }
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const value = useContext(AuthContext);
if (value === null) {
throw new Error('useAuth must be used inside an AuthProvider');
}
return value;
}
The check in useAuth is a helpful habit. If you forget to wrap your app in the provider, you get a clear error message instead of a confusing undefined.
function Header() {
const { user, logout } = useAuth();
return user ? (
<p>Hello {user.name} <button onClick={logout}>Log out</button></p>
) : (
<p>Please sign in.</p>
);
}
How context finds its value
useContext looks up the tree for the nearest provider of that context. You can even put a provider inside another one to change the value for one part of the page only. If there is no provider above, you get the default value from createContext(defaultValue).
A performance warning
When the value in a provider changes, every component that reads that context draws again. In the examples above we wrote value={{ theme, toggleTheme }}, which makes a new object on every draw of the provider, so all readers redraw even when nothing really changed. For small apps this is fine. In bigger apps you can keep the value steady with useMemo, which lesson 21 covers:
const value = useMemo(() => ({ user, login, logout }), [user]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
- Split big contexts: one for the user, one for the theme, one for the cart. A change in one does not redraw the readers of the others.
- Keep rarely changing and often changing data in different contexts.
- Put the provider as low in the tree as it can be, only around the parts that need it.
Context and useReducer together
For bigger shared state, combine them. The reducer holds the rules, and the context shares the state and dispatch:
const CartContext = createContext(null);
export function CartProvider({ children }) {
const [cart, dispatch] = useReducer(cartReducer, []);
return (
<CartContext.Provider value={{ cart, dispatch }}>
{children}
</CartContext.Provider>
);
}
This is a small, complete state manager with no extra library. For very large apps or special needs you can reach for Redux Toolkit or Zustand, which lesson 24 introduces.
Context is not a database
Context only shares state with components. The state itself still lives in useState or useReducer inside the provider. If you need data from a server, load it (lesson 12) and then put the result in the provider, or use a data library (lesson 25).
Common mistakes
- Forgetting the provider, so the component gets the default value (often
null). - Putting everything in one giant context, so every change redraws everything.
- Using context for data that only one parent and child share, where props are simpler.
- Making the value a new object every draw without thinking about performance in a larger app.
- Trying to change a context value directly. You must call the function the provider shares, which changes the provider's state.
Following the value through the tree
It is useful to picture what happens when someone clicks the theme button. The button lives deep inside the page, and the provider is at the very top.
- The button calls
toggleTheme, a function that came from the context. - That function belongs to the provider, so it calls
setThemein the provider's own state. - The provider's state changes, so the provider renders again, and puts a new
valueinto the context. - React finds every component that called
useContextfor this context, and draws them again with the new value. - Components that do not read the context are left alone, unless their parent redrew.
This shows an important point: the state still lives in an ordinary useState. The context is only the delivery system that brings the value, and the functions to change it, to every place that needs them. You can think of the provider as a small power station, and useContext as the plug in each room.
A rule of thumb for choosing
- If you pass a prop through one or two levels, keep passing props. It is clear and simple.
- If you pass the same prop through three or more levels that do not use it, try composition first.
- If many unrelated components need the same value, such as the user or the theme, use context.
Questions beginners often ask
Can I have more than one provider? Yes, as many as you need. You can nest them, for example a theme provider inside an auth provider. Each context is separate.
Where does the provider go? Around the parts of the app that need the data. Often that is main.jsx, wrapped around App. You can also place a provider lower in the tree for one section.
Can I change context from a child? Yes, but through a function that the provider shares, such as toggleTheme. The provider owns the state, so changes go through it.
Is context only for global data? It is for data that many components share. It does not have to be for the whole app. A form with many parts can have a small context just for itself.
Your task
- Build a
ThemeProviderwith a toggle button, and show the theme name in two different components. - Build an
AuthProviderwithloginandlogout, and show different content in a header for each case. - Make
useThemethrow a helpful error when used outside a provider. - Create a
CartProviderwithuseReducerand show the number of items in the header and in a cart page.
How to check it worked
- Both components change when you press the toggle.
- Using the hook outside a provider shows your clear error message.
- The cart count in the header always matches the cart page.
Summary
- Context shares data with any component below a provider, with no prop drilling.
- Steps:
createContext, wrap withProvider value={...}, read withuseContext. - Wrap the context in a custom hook, and check that a provider exists.
- A changed value redraws every reader. Split contexts and keep values steady in big apps.