Context — Sharing State Without Prop Drilling
The Problem: Prop Drilling
The current user is needed by a button five levels deep. Passing it through four components that don’t care is prop drilling — noisy and brittle. Context lets a parent provide a value and any descendant consume it directly.
The Three Steps
import { createContext, useContext, useState } from "react";
// 1. create
const ThemeContext = createContext(null);
// 2. provide — wrap a subtree, pass the value
function App() {
const [theme, setTheme] = useState("dark");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Layout />
</ThemeContext.Provider>
);
}
// 3. consume — anywhere below, no props needed
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
{theme === "dark" ? "☀️" : "🌙"}
</button>
);
}When the provider’s value changes, every consuming component re-renders.
The Production Pattern: Provider + Hook Module
Real apps wrap each context in its own file with a custom hook — clean API, good errors:
// AuthContext.jsx
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
async function login(email, password) {
const u = await api("/login", { method: "POST", body: { email, password } });
setUser(u);
}
function logout() { setUser(null); }
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used inside <AuthProvider>");
return ctx;
}// anywhere in the app:
const { user, logout } = useAuth();
{user ? <button onClick={logout}>Log out</button> : <LoginLink />}Providers nest at the root: <AuthProvider><ThemeProvider><App />…
When Context Is Right — and Wrong
Right: truly app-wide, slowly-changing data — current user, theme, language, feature flags.
Wrong:
- Passing props down one or two levels — just pass props; drilling a little is fine and explicit.
- Fast-changing data (every keystroke, mouse position) — every consumer re-renders on every change.
- A grab-bag “AppContext” holding everything — split contexts by concern so consumers only re-render for data they use.
For complex client state beyond context’s comfort zone, the community reaches for Zustand or Redux Toolkit; for server data, TanStack Query. Learn context first — the others build on the same provider/consumer mental model.