Themes, the current user, locale and similar app-wide values need to reach distant components cleanly.
Passing props through many layers that don't use them (prop drilling) gets tedious. Context lets a parent make a value available to every component below it, however deep.
Three steps:
const ThemeContext = createContext("light"); // 1. create, with a default
<ThemeContext value="dark"><Page /></ThemeContext> // 2. provide
const theme = useContext(ThemeContext); // 3. readIn React 19 you render the context itself as the provider (<ThemeContext value=...>); <ThemeContext.Provider> still works but is on its way out. A component reads the value from the nearest provider above it; with none, it gets the default from createContext. Providers can be nested to override values for a subtree.
React 19's use(ThemeContext) reads context too, and unlike useContext it can be called inside conditions and loops.
When the provided value changes, every component reading that context re-renders. Two consequences:
- If you provide an object,
value={{ user, setUser }}creates a new object each render, re-rendering all consumers. Memoize it withuseMemo(or let the React Compiler do it), or split it into separate contexts for rarely and frequently changing parts. - Context isn't a substitute for a state manager with fine-grained subscriptions; for large, fast-changing shared state consider an external store.
A very common pattern is context plus a reducer: keep state in a top-level component with useReducer and provide both state and dispatch via context, wrapped in a custom Hook like useTasks().
Before reaching for context, try composition: passing JSX as children often removes the drilling with no new concepts.
Example
import { createContext, use, useContext, useState } from "react";
const ThemeContext = createContext("light");
const UserContext = createContext(null);
function Button({ children }) {
const theme = useContext(ThemeContext);
return <button className={`btn btn-${theme}`}>{children}</button>;
}
function Greeting({ show }) {
if (!show) return null;
const user = use(UserContext); // use() may follow an early return
return <p>Signed in as {user.name}</p>;
}
export default function App() {
const [theme, setTheme] = useState("light");
return (
<UserContext value={{ name: "Ada" }}>
<ThemeContext value={theme}>
<Greeting show />
<Button>Save</Button>
<ThemeContext value="dark">
<Button>Always dark</Button>
</ThemeContext>
<button onClick={() => setTheme(t => (t === "light" ? "dark" : "light"))}>Toggle</button>
</ThemeContext>
</UserContext>
);
}Remember
createContext(default), provide with<Ctx value={...}>, read withuseContext(Ctx)oruse(Ctx)- Consumers read the nearest provider above them and re-render when its value changes
- Keep provided objects stable and split contexts by update frequency
Watch out for
- Calling
useContextin the same component that renders the provider: it reads the provider above, not its own.