React’s Context API solves prop drilling — the tedious pattern of passing a value through many intermediate components just to reach a deeply nested consumer. Authentication state is the canonical example: the logged-in user’s name is needed in the header, the dashboard, and the post editor, but passing it from the root through every layout and page component is impractical. Context lets you place a value in a “tree-wide scope” and have any component consume it directly, without threading the prop through intermediaries.
Creating and Using Context
import { createContext, useContext, useState } from "react";
// ── 1. Create the context (and a custom hook to consume it) ───────────────────
const ThemeContext = createContext(null); // null = no default value
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used inside ThemeProvider");
return ctx;
}
// ── 2. Create the Provider ────────────────────────────────────────────────────
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
function toggleTheme() {
setTheme((t) => (t === "light" ? "dark" : "light"));
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// ── 3. Wrap the app ───────────────────────────────────────────────────────────
// src/main.jsx:
// <ThemeProvider><BrowserRouter><App /></BrowserRouter></ThemeProvider>
// ── 4. Consume in any component without prop drilling ─────────────────────────
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={theme === "dark" ? "bg-gray-900 text-white" : "bg-white"}>
<button onClick={toggleTheme}>Toggle theme</button>
</header>
);
}
Note: Every component that calls
useContext(ThemeContext) re-renders whenever the context value changes. If the context value is an object created inline in the Provider — value={{ theme, toggleTheme }} — a new object is created on every render of the Provider, even when theme has not changed. This causes all consumers to re-render unnecessarily. Fix: memoize the value with useMemo, or split the context into a “state context” and an “actions context” so stable actions do not trigger re-renders.Tip: Always export a custom
useXxx hook that wraps useContext rather than exporting the context object directly. The hook can throw a helpful error if used outside the provider (if (!ctx) throw new Error("useTheme must be inside ThemeProvider")), and it hides the implementation detail that this particular value comes from a context — you can later replace the context with a Zustand store without changing any consumer components.Warning: Context is not a replacement for all state management. It is appropriate for infrequently-changing global values: the logged-in user, the active theme, a locale preference. For frequently-changing data (a list of posts that updates on every filter change), or for complex update logic, a dedicated store (Zustand, Redux Toolkit) or a data-fetching library (TanStack Query) is more appropriate. Using context for high-frequency updates causes performance problems because every consumer re-renders on every change.
When to Use Context vs Alternatives
| Situation | Best Tool | Reason |
|---|---|---|
| Auth user, theme, locale | Context API | Infrequent changes, app-wide access |
| Two sibling components share state | Lift state up | Simple, no library needed |
| Server data (posts, comments) | TanStack Query | Caching, deduplication, background refetch |
| Complex UI state (cart, multi-step form) | Zustand or useReducer | Predictable updates, devtools |
| Frequently updated UI state | Zustand | Context re-renders all consumers |
Common Mistakes
Mistake 1 — Inline object in Provider value (unnecessary re-renders)
❌ Wrong — new object on every render triggers all consumers:
function Provider({ children }) {
const [user, setUser] = useState(null);
return (
<Ctx.Provider value={{ user, setUser }}> {/* new object every render! */}
{children}
</Ctx.Provider>
);
}
✅ Correct — memoize the value:
import { useMemo } from "react";
const value = useMemo(() => ({ user, setUser }), [user]);
return <Ctx.Provider value={value}>{children}</Ctx.Provider>; // ✓
Mistake 2 — Using context without a custom hook (no error on misuse)
❌ Wrong — silent null if used outside provider:
export const AuthContext = createContext(null);
// Usage: const ctx = useContext(AuthContext) — ctx is null outside provider, crashes later
✅ Correct — export a hook with a guard:
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used inside AuthProvider");
return ctx;
}
Quick Reference
| Step | Code |
|---|---|
| Create context | const Ctx = createContext(null) |
| Provide value | <Ctx.Provider value={memoizedValue}>{children}</Ctx.Provider> |
| Consume value | const value = useContext(Ctx) |
| Custom hook | export function useCtx() { const v = useContext(Ctx); if (!v) throw ...; return v; } |