Share Data Without Threading Props Through Every Layer
useContext
Context lets a deep child read a value from any ancestor without intermediate components having to pass it through.
What you'll learn
- Create a context with `createContext`
- Provide a value with `<Context.Provider>`
- Read it from any descendant with `useContext`
When the same value is needed in many components — a theme, the current user, a translator function — passing it through every layer as a prop gets painful. Context is the React way to skip that.
Three Pieces
- Create a context
- Wrap part of the tree in
<Context.Provider value={...}> - Read the value with
useContextfrom any descendant
Step By Step
// 1. Create
const ThemeContext = createContext("light");
// 2. Provide
function App() {
const [theme, setTheme] = useState("dark");
return (
<ThemeContext.Provider value={theme}>
<Page />
</ThemeContext.Provider>
);
}
// 3. Read
function Button() {
const theme = useContext(ThemeContext);
return <button className={`btn btn--${theme}`}>OK</button>;
} Button reads theme even though App never passed it as a prop
to anyone — context skipped the layers.
Defaults
The argument to createContext is the value seen by consumers that
are NOT wrapped in a provider:
const ThemeContext = createContext("light");
// no provider above:
useContext(ThemeContext); // "light" A real default makes a component usable in isolation (tests, stories) without setting up a provider just to render it.
Passing Setters Too
Context can carry a full object — including functions to update state.
const ThemeContext = createContext(null);
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const value = { theme, setTheme };
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Toggle
</button>
);
} Recommended Wrapper Pattern
Wrap the raw context with a custom hook + a provider component. Consumers shouldn’t see the raw context at all.
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used inside ThemeProvider");
return ctx;
} Now in components: const { theme, setTheme } = useTheme();. No
imports of the raw context anywhere.
Performance Note
When the provider’s value changes, every consumer re-renders.
That means every component using useContext for this context.
For lightweight values that change rarely (theme, locale) this is
fine. For frequently-changing state shared with many components,
consider splitting into multiple contexts or using a dedicated
state library.
When To Use Context
Good fits:
- Theme, locale, current user
- A dependency-injected service (logger, analytics)
- “Slot” patterns inside complex components
Not great:
- General app state — a state library (Zustand, Redux) is better at scale
- Frequently changing values used by many components
- Things only one component needs — just pass a prop
Up Next
When state logic gets complex, useReducer is the upgrade from
useState.