The Problem: Deep Prop Drilling
Some data — like the current theme, logged-in user, or language — is needed by many components at very different depths in the tree. Passing it down as props through every intermediate component (prop drilling) becomes tedious and fragile as the app grows.
Creating a Context
createContext() creates a Context object. A Provider component makes a value available to every component nested inside it, no matter how deep.
Creating and Providing a Context
import { createContext } from "react";
const ThemeContext = createContext("light");
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}Reading a Context Value
Any component nested inside the Provider can read the current value with the useContext hook, regardless of how many components sit between it and the Provider.
Consuming Context with useContext
import { useContext } from "react";
function Toolbar() {
return <ThemedButton />; // doesn't need to know about ThemeContext at all
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click me</button>;
}Combining Context with useState
Context is most useful when combined with state — the Provider’s value can include both the current data and a function to update it, giving any nested component read and write access.
A Context with Updatable State
const UserContext = createContext(null);
function App() {
const [user, setUser] = useState({ name: "Alice" });
return (
<UserContext.Provider value={{ user, setUser }}>
<Dashboard />
</UserContext.Provider>
);
}
function Dashboard() {
const { user, setUser } = useContext(UserContext);
return <button onClick={() => setUser({ name: "Bob" })}>{user.name}</button>;
}Every Consumer Re-Renders on Change
When a Provider’s value changes, every component consuming that context re-renders, even if it only cares about part of the value. For frequently-changing, performance-sensitive state, a dedicated state management library or splitting into smaller contexts may be a better fit.
Context is Not a Replacement for All Props
Context works best for data considered "global" to a whole subtree — theme, auth, locale. For data that only a couple of nearby components need, regular props (or lifting state up) is usually simpler and easier to trace.
Best Practice
Wrap the Provider and its useContext logic in a small custom hook (e.g. useTheme()) so consuming components import one clean function instead of dealing with useContext(ThemeContext) directly everywhere.