The Context API Explained
Sharing data across many components without passing it down through every layer of props in between.
2 min read
Lifting state up works well when the components sharing a value are close together. It breaks down for data that dozens of components scattered across the tree all need — a logged-in user, a theme, a locale — because every intermediate component has to accept and forward a prop it never actually uses itself, just to relay it further down. This is prop drilling, and Context is React's built-in answer to it.
Creating and providing context
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext(null);
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}createContext makes a Context object; wrapping part of the tree in <ThemeContext.Provider value={...}> makes that value available to every component nested inside it, no matter how deep.
Consuming context
Any descendant reads the value with useContext, regardless of how many components sit between it and the Provider:
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
className={theme}
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
Toggle theme
</button>
);
}ThemedButton can sit anywhere under App — inside Toolbar, inside something Toolbar renders, arbitrarily deep — and none of the components in between need to know theme exists or pass it along.
What Context is not
Context solves prop drilling, not general state management. It's easy to reach for it as a default and end up with problems it isn't built to solve:
- Every consumer re-renders when the context value changes, even if a given consumer only cares about part of it. Passing a large, frequently changing object through one Context can cause far more re-rendering than passing several smaller, more targeted contexts (or props) would.
- Context has no built-in way to update state from outside the Provider component — you're still using
useStateoruseReducerunderneath; Context only handles the distribution, not the state logic itself. - For state with complex update logic shared across a large app, a dedicated state management library (Redux, Zustand, Jotai) is usually a better fit than a hand-rolled Context — they add more efficient update propagation than Context provides on its own.
The rule of thumb: reach for Context when you're passing the same prop through three or more layers of components that don't use it themselves. For state that's local to a small part of the tree, plain props and lifting state up, as covered earlier, are usually simpler and easier to trace.
The next lesson covers extracting reusable logic — including logic that uses Context — into your own custom hooks.