The problem it solves
Three components carry a prop they do not care about. Add a second value and every signature grows again.
<Page theme={theme}>
<Layout theme={theme}> {/* does not use it */}
<Sidebar theme={theme}> {/* does not use it */}
<Badge theme={theme} /> {/* finally */}
Create, provide, read
// 1. create it, with a default for when there is no provider
const ThemeContext = React.createContext({ theme: "standard" });
// 2. provide a value to a subtree
<ThemeContext.Provider value={value}>
<Layout />
</ThemeContext.Provider>
// 3. read it at any depth
function Badge() {
const { theme } = React.useContext(ThemeContext);
return <span className={"badge badge--" + theme} />;
}
<ThemeContext> works directly as the provider — .Provider is no longer required.
The default is not a fallback for a missing value
A silent default is often worse than a loud failure — the component renders, looks slightly wrong, and nobody knows why. Many codebases make the default null and throw from a custom hook instead.
const ThemeContext = React.createContext({ theme: "standard" });
// outside any provider → { theme: "standard" }
// inside a provider with value={undefined} → undefined, not the default
The custom-hook wrapper
- Components import
useTheme, never the context object. - A missing provider fails immediately, with a sentence that says what to do.
- You can change how the value is produced without touching a single consumer.
const ThemeContext = React.createContext(null);
export function useTheme() {
const value = React.useContext(ThemeContext);
if (value === null) throw new Error("useTheme must be used inside <ThemeProvider>");
return value;
}
The cost: every consumer re-renders
React.memo does not help here: a context update reaches consumers regardless of props. The only defences are a stable value and splitting the context.
// value is a NEW object on every render of Provider,
// so every consumer in the tree re-renders — even if theme never changed
<ThemeContext.Provider value={{ theme, setTheme }}>
// memoise it
const value = React.useMemo(() => ({ theme, setTheme }), [theme]);
<ThemeContext.Provider value={value}>
Split by how often it changes
Components that only dispatch — every button in the app — then never re-render when the state changes. This pairing of a reducer with two contexts is the standard way to build a small store without a library.
// changes constantly
<OrderStateContext.Provider value={state}>
{/* stable forever: dispatch never changes identity */}
<OrderDispatchContext.Provider value={dispatch}>
{children}
</OrderDispatchContext.Provider>
</OrderStateContext.Provider>
What context is and is not
- Is: a way to pass a value down without props. That is the whole feature.
- Is not: a state manager. It does not cache, batch, select a slice or prevent re-renders.
- Good fits: theme, locale, the current user, a toast function, a router, a form instance.
- Bad fits: server data (use a query cache), high-frequency values like mouse position or every keystroke.
Before reaching for it
Two levels of prop drilling is not a problem worth a context. Passing children often removes the need entirely, because the element is created where the data already lives. Context earns its place when many components at unpredictable depths need the same thing.