🎓 What You Will Learn
- The prop drilling problem: why passing props through 4+ levels hurts
- createContext + Provider: making a value available to a whole subtree
- useContext: reading that value from any descendant, at any depth
- Context + useState: sharing state that other components can update
- Custom hook wrapper: the
useAuth()/useCart()pattern - When NOT to use context: the cases where prop drilling — or a state library — wins
The Prop Drilling Problem
Props flow one way: parent to child. That's fine until a deeply nested component needs a value that only the top-level component has. You end up threading that prop through every component in between — components that don't use it, they just pass it along.
function App() {
const [theme, setTheme] = useState("light");
return <Page theme={theme} />;
}
function Page({ theme }) {
return <Sidebar theme={theme} />; // Page never uses theme itself
}
function Sidebar({ theme }) {
return <UserCard theme={theme} />; // neither does Sidebar
}
function UserCard({ theme }) {
return <div className={theme}>...</div>; // finally, someone uses it
}This is prop drilling. It's not wrong, but it makes every middle component depend on a prop it doesn't care about, and renaming or restructuring becomes painful. Context solves exactly this.
createContext + Provider + useContext
Three pieces: createContext() makes a context object, <Context.Provider value={...}> makes a value available to every component inside it, and useContext(Context) reads that value from anywhere in that subtree — no matter how many components sit in between.
This card reads theme straight from context — no props were passed to it.
ThemedCardWrapper renders <ThemedCard /> without ever mentioning theme. That's the entire point — middle components stay clean.Context + useState — Shared, Updatable State
A static value is nice, but most real contexts pair with useState so descendants can both read and update shared state — the value you provide is just an object holding the state and the functions that change it.
useAuth()) does two jobs: it hides useContext(AuthContext) behind a friendlier name, and it throws a clear error if someone forgets to wrap their tree in the Provider — instead of a confusing null crash three components later.Sharing State Across Unrelated Branches
Context really earns its keep when two components that aren't parent/child — like a header badge and a product card in a completely different part of the tree — need the same live state.
Composing Multiple Contexts
Real apps usually have more than one context (theme, auth, cart...). Nest their Providers, or write one combined AppProviders component to keep your root tidy.
function AppProviders({ children }) {
return (
<ThemeContext.Provider value={theme}>
<AuthContext.Provider value={authValue}>
<CartContext.Provider value={cartValue}>
{children}
</CartContext.Provider>
</AuthContext.Provider>
</ThemeContext.Provider>
);
}
function App() {
return (
<AppProviders>
<Page />
</AppProviders>
);
}When NOT to Reach for Context
useContext re-renders whenever the Provider's value changes — even if the component only cares about one field of a large object.Skip context (or split it into smaller contexts) when:
- The value changes very frequently (e.g. mouse position, a text input on every keystroke) and many components consume it
- You only need to pass a prop 1–2 levels — that's not drilling, that's just props
- You need selective re-renders, time-travel debugging, or middleware — reach for Zustand or Redux Toolkit instead
Common Mistakes
// ❌ BAD: no <AuthContext.Provider> anywhere — useAuth() throws or returns the default
function App() {
return <ProfileMenu />;
}
// ✅ GOOD: wrap the tree that needs the value
function App() {
return (
<AuthContext.Provider value={authValue}>
<ProfileMenu />
</AuthContext.Provider>
);
}// ❌ BAD: one context for theme + auth + cart —
// changing the cart count re-renders every theme consumer too
const AppContext = createContext({ theme, user, cart });
// ✅ GOOD: separate contexts, so a cart update
// doesn't re-render components that only read theme
const ThemeContext = createContext(theme);
const AuthContext = createContext(user);
const CartContext = createContext(cart);// ❌ BAD: a brand-new object every render — every consumer re-renders
<AuthContext.Provider value={{ user, login, logout }}>
// ✅ GOOD: memoize it so the reference is stable across renders
const value = useMemo(() => ({ user, login, logout }), [user]);
<AuthContext.Provider value={value}>Best Practices
useAuth, useCart) with a helpful errorProvider to avoid needless re-rendersPattern Reference
| Piece | Purpose |
|---|---|
| createContext(default) | Creates the context object, with an optional fallback value |
| <Context.Provider value={...}> | Makes a value available to every descendant inside it |
| useContext(Context) | Reads the current value from the nearest Provider above |
| Custom hook wrapper | Hides useContext + throws if used outside a Provider |
| useMemo(() => value, [deps]) | Keeps the Provider's value reference stable |
Practice Project
Build a small language switcher that:
- Creates a
LanguageContextholding the current language + a setter - Wraps a
useLanguage()custom hook around it with a "must be inside Provider" error - Has a language picker in the header and a greeting deep in the page — both consuming the same context
- Switching the language updates the greeting instantly, with zero props passed between them
What's Next?
- useReducer: pair with context for more complex, action-based state
- Custom Hooks: extract more reusable logic beyond
useAuth/useCart - State Management: when to graduate from context to Zustand or Redux Toolkit
- useMemo / React.memo: control re-renders once context consumers grow
Keep practicing! 💪 Context is one of the biggest "aha" moments in React — once prop drilling stops being your default answer, whole trees of components get simpler to write and refactor.