React Context API
Context lets you share data across a component tree without passing it through every level as props. It is the standard React solution for things like the current user, a theme, or a language setting.
Before reaching for a state management library, check whether Context solves your problem. For many apps, it does.
The prop drilling problem
Suppose your app has a user object at the top level. Several components deep in the tree need to display the user’s name. Without Context, you pass user down through every component in between, even the ones that do not use it directly.
function App() {
const user = { name: 'Anna', role: 'admin' };
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <UserMenu user={user} />;
}
function UserMenu({ user }) {
return <p>Logged in as: {user.name}</p>;
}
Layout and Sidebar receive user only to pass it along. They do not use it themselves. This is prop drilling: the data has to travel through every intermediate component to reach the one that needs it.
Context removes those intermediate hand-offs.
Creating a context
Call createContext with a default value. The default is used when a component reads the context but has no Provider above it in the tree.
// ThemeContext.js
import { createContext } from 'react';
export const ThemeContext = createContext('light');
Export the context so any component in the app can import it.
Providing a context value
Wrap the part of the tree that needs access to the value in a Context.Provider. Any component inside the Provider can read the context value, no matter how deeply nested.
import { ThemeContext } from './ThemeContext';
function App() {
return (
<ThemeContext.Provider value="dark">
<Layout />
</ThemeContext.Provider>
);
}
To make the value dynamic, store it in state and pass the state variable as the value prop:
import { useState } from 'react';
import { ThemeContext } from './ThemeContext';
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Layout />
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle theme
</button>
</ThemeContext.Provider>
);
}
Reading a context value with useContext
Any component inside the Provider can call useContext to read the value. No props needed.
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
function ThemedCard() {
const theme = useContext(ThemeContext);
const style = {
background: theme === 'dark' ? '#333' : '#fff',
color: theme === 'dark' ? '#fff' : '#333',
padding: '16px',
borderRadius: '8px',
};
return <div style={style}>Current theme: {theme}</div>;
}
ThemedCard does not receive any props. It reads the theme directly from Context and re-renders whenever the Provider’s value changes.
A complete working example
Here is a full theme toggle built with Context. Four components, no prop drilling.
// ThemeContext.js
import { createContext } from 'react';
export const ThemeContext = createContext('light');
// App.jsx
import { useState } from 'react';
import { ThemeContext } from './ThemeContext';
import ThemeToggle from './ThemeToggle';
import ThemedCard from './ThemedCard';
function App() {
const [isDark, setIsDark] = useState(false);
const theme = isDark ? 'dark' : 'light';
return (
<ThemeContext.Provider value={{ theme, setIsDark }}>
<div style={{ padding: '24px' }}>
<ThemeToggle />
<ThemedCard title="Hello" body="This card reads the theme from Context." />
<ThemedCard title="Another card" body="Same Context, different component." />
</div>
</ThemeContext.Provider>
);
}
export default App;
// ThemeToggle.jsx
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
function ThemeToggle() {
const { theme, setIsDark } = useContext(ThemeContext);
return (
<button onClick={() => setIsDark((prev) => !prev)}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
);
}
export default ThemeToggle;
// ThemedCard.jsx
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
function ThemedCard({ title, body }) {
const { theme } = useContext(ThemeContext);
const style = {
background: theme === 'dark' ? '#1e1e1e' : '#f5f5f5',
color: theme === 'dark' ? '#f0f0f0' : '#111',
padding: '16px',
borderRadius: '8px',
marginTop: '12px',
};
return (
<div style={style}>
<h3>{title}</h3>
<p>{body}</p>
</div>
);
}
export default ThemedCard;
The Provider passes an object with both the current theme and the setter. Both ThemeToggle and ThemedCard can read from it with useContext, and neither needs props from App.
What Context is good for
Context works well for data that many components across the tree need to read:
- Current logged-in user and authentication state
- App-wide theme (light/dark mode)
- Language and locale settings
- Global notification or toast state
- Feature flags
What Context is not good for
Context is not a drop-in replacement for all state management.
Do not use it for data that only one or two components need. If only a parent and a direct child share a value, props are simpler and more explicit.
Do not use it for values that change frequently. Every component that calls useContext re-renders when the Provider’s value changes. If the value updates many times per second (like a cursor position or scroll offset), Context will cause performance problems. Use a more targeted approach for those cases.
Do not use it for server data. If you are fetching users, posts, or products from an API, Context does not handle caching, background refetching, or stale data. Use TanStack Query for that.
Context vs useState vs a library
| Situation | What to use |
|---|---|
| Data used only by one component | useState inside that component |
| Data shared between a parent and one or two children | Lift state up, pass via props |
| Data needed across many components in a subtree | Context |
| Server data (API responses, caching, sync) | TanStack Query |
| Complex global client state across many features | Zustand or Redux Toolkit |
Start with the simplest option and reach for Context only when props become genuinely unwieldy.
Common mistakes
Forgetting the Provider.
If a component calls useContext but has no Provider above it in the tree, it gets the default value passed to createContext. This is often undefined or an empty string, which causes silent bugs. Always check that your Provider wraps the component that needs the context.
One giant context for everything.
If you put unrelated values into one context (theme, user, cart, language), every component that reads any one of those values will re-render whenever any of them changes. Split unrelated values into separate contexts.
Using Context for high-frequency updates.
Animating a value, tracking mouse position, or responding to scroll events at 60fps through Context will cause performance problems. Use a ref or a dedicated library for those cases.
FAQ
Does the default value in createContext matter?
Only when a component reads the context without a Provider anywhere above it in the tree. In practice, you almost always have a Provider at the top of your app, so the default is rarely used. It can be useful for testing individual components in isolation.
Can I have multiple Providers for the same context?
Yes. A Provider lower in the tree will override the one above it for everything nested inside it. This is useful for section-specific themes or temporary overrides.
Should I always export a custom hook to wrap useContext?
It is a good pattern for larger codebases: export a useTheme() function that calls useContext(ThemeContext) internally. This lets you add error checking (throwing if used outside the Provider) and keeps the context import out of every consumer file. For small projects, calling useContext directly is fine.
What to read next
- Lifting State Up : the step before Context, and what leads you to need it
- useState : the hook that drives most Context values
- useReducer : a good pairing with Context for more complex state logic