Topics in this subject
React 4 min read Updated 5 Aug 2026

useContext & Context API

Context shares values without prop drilling; every consumer re-renders on value change—memoize and split.

Context passes data through the tree without threading props at every level. It solves prop drilling, not global state management (it has no built-in reducers, persistence, or selectors).

The three pieces#

// 1. Create — the argument is the default used ONLY when no Provider is above
const ThemeContext = createContext("light");

// 2. Provide — everything below reads `value`
function App() {
  const [theme, setTheme] = useState("dark");
  return (
    <ThemeContext.Provider value={theme}>
      <Toolbar onToggle={() => setTheme(t => (t === "dark" ? "light" : "dark"))} />
    </ThemeContext.Provider>
  );
}

// 3. Consume — reads the NEAREST Provider's value, subscribes to changes
function Button() {
  const theme = useContext(ThemeContext); // "dark"
  return <button className={theme}>Save</button>;
}

🎯 The defaultValue in createContext is used only when a consumer has no matching Provider ancestor—useful for tests/storybook, a common trap in "why is my value undefined" bugs (you forgot the Provider).

Provider → consumers#

flowchart TD
    P["ThemeContext.Provider value={theme}"] --> A["Layout"]
    A --> B["Toolbar"]
    A --> C["Sidebar"]
    B --> D["Button (useContext)"]
    C --> E["Panel (useContext)"]
    P -.->|"value changes"| D
    P -.->|"value changes"| E

The re-render gotcha 🎯⚠️#

Every consumer re-renders when the context value changes, regardless of React.memomemo compares props, and context is not a prop. Two failure modes:

// ⚠️ New object EVERY render → all consumers re-render every parent render
<UserContext.Provider value={{ user, setUser }}>

// 🟢 Memoize the value so its identity is stable
const value = useMemo(() => ({ user, setUser }), [user]);
<UserContext.Provider value={value}>

🟢 Split contexts by change frequency. If user rarely changes but theme toggles often, one combined context re-renders user consumers on every theme flip. Separate them so each consumer subscribes only to what it needs. A frequent pattern: split state and dispatch into two contexts—dispatch is stable, so action-only components never re-render on state changes.

const StateCtx = createContext(null);
const DispatchCtx = createContext(null);
// components that only dispatch subscribe to DispatchCtx → never re-render on state

React 19 note#

React 19 lets you render <ThemeContext> directly as the provider (the .Provider suffix becomes optional). Both forms are valid in 19; use .Provider for 18 compatibility.

<ThemeContext value={theme}>…</ThemeContext> // React 19

Context vs a state library#

Concern Context API Redux / Zustand / Jotai
Purpose Dependency injection (pass value down) State management + updates
Selective subscription ❌ all consumers re-render ✅ selectors / atoms
Middleware, devtools
Async / thunks Roll your own Built-in patterns
Boilerplate Minimal More (less with Zustand/Jotai)
Best for Theme, locale, auth user, current tenant Large, frequently-updated, cross-cutting state

🟢 Reach for context for low-frequency, tree-wide values (theme, locale, auth). Reach for a store when you need selective subscriptions or high-frequency updates—context has no way to say "only re-render if state.count changed."

Interview Q&A#

Q1. Does React.memo prevent a component from re-rendering when context changes? No. memo only short-circuits prop changes. A useContext consumer always re-renders when its context value's identity changes, even if wrapped in memo.

Q2. Why does wrapping the provider value in an object cause performance issues? An inline object literal is a new reference every render, so the context value's identity changes each time the provider re-renders, forcing all consumers to re-render. Memoize the value with useMemo.

Q3. When would you split one context into several? When different consumers care about different slices that change at different rates. Splitting lets each consumer subscribe only to the slice it uses, avoiding re-renders triggered by unrelated updates (e.g. separate state and dispatch contexts).

Q4. What is the defaultValue argument to createContext actually for? It is returned only when a consumer has no matching Provider above it. In a properly wrapped app it is never used at runtime; it mainly helps isolated tests and Storybook, and documents the shape.

Q5. When should you choose a state library over context? When you need selective subscriptions (re-render only on the slice you use), high-frequency updates, devtools, middleware, or complex async flows—things context does not provide out of the box.