C.W.K.
Stream
Lesson 05 of 07 · published

useContext — Sharing State Across the Tree

~14 min · usecontext, context, providers

Level 0React Novice
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
Context is React's prop-drilling escape hatch. Use it for things that are genuinely tree-wide (theme, auth, locale) — not for everything that's annoying to thread through three levels.

The three parts

  1. createContext(defaultValue) — returns a Context object (and in React 19, the Context itself is the Provider, no .Provider needed).
  2. The Provider — wraps a subtree and supplies the current value: <MyCtx value={...}>.
  3. useContext(MyCtx) — reads the nearest provider's value from any descendant.

The default value (the argument to createContext) is what consumers see when no Provider is above them. Use it for nullable defaults; many apps wrap consumers in a hook that throws if the Provider is missing (see code block).

React 19's <Context> as Provider

Before React 19 you'd write <MyCtx.Provider value={...}>. In 19 the Context object can be used directly as the Provider — <MyCtx value={...}>. Both still work; the new syntax is shorter and matches how callers think about the value.

Re-renders

When a Provider's value changes (reference equality), every consumer re-renders. If your value is an object built in render — value={{ theme, setTheme }} — it changes every render and every consumer re-renders. Memoize the object with useMemo, or trust the React Compiler (Track 7) to do it for you.

When to reach for Context

  • Theme (light/dark, accent color).
  • Auth (current user, login/logout functions).
  • Locale and translations.
  • Compound-component state (lesson 3 of Track 2 showed this).
  • App-wide settings that genuinely need to be tree-wide.

When not to

  • Server state (fetched data) — use a query library (TanStack Query) or your own custom hook.
  • Frequently-changing values where most consumers don't care — Context will re-render everyone.
  • Form state — keep it local or in a Zustand store.
Context isn't a state-management library. It's a prop-bypass mechanism. If you need cross-component state with sophisticated update patterns, reach for Zustand, Jotai, or Redux Toolkit. Context's job is delivery — not state architecture.

Code

Theme context with the safe-consumer pattern·tsx
import { createContext, useContext, useMemo, useState } from "react";

type Theme = "light" | "dark";
type ThemeCtxValue = { theme: Theme; setTheme: (t: Theme) => void };

// Default is null — consumers must be inside a Provider.
const ThemeCtx = createContext<ThemeCtxValue | null>(null);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>("dark");
  // Memoize so the object reference is stable across renders.
  const value = useMemo(() => ({ theme, setTheme }), [theme]);
  return <ThemeCtx value={value}>{children}</ThemeCtx>;
}

// Custom hook is the canonical consumer — throws if the Provider is missing.
export function useTheme(): ThemeCtxValue {
  const ctx = useContext(ThemeCtx);
  if (!ctx) throw new Error("useTheme must be used inside <ThemeProvider>");
  return ctx;
}

// Caller — no explicit useContext, no missing-Provider risk.
function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  return (
    <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
      Switch to {theme === "dark" ? "light" : "dark"}
    </button>
  );
}
The useMemo trap·tsx
// WRONG — value object is fresh every render, every consumer re-renders.
function BadProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>("dark");
  return (
    <ThemeCtx value={{ theme, setTheme }}>
      {children}
    </ThemeCtx>
  );
}

// RIGHT — memoize the value, reference is stable until theme changes.
function GoodProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>("dark");
  const value = useMemo(() => ({ theme, setTheme }), [theme]);
  return <ThemeCtx value={value}>{children}</ThemeCtx>;
}

// (The React Compiler, Track 7 lesson 4, will memoize this for you automatically
// in projects where the compiler is enabled.)

External links

Exercise

Build a <LocaleProvider> that holds { locale: 'en' | 'ko'; t: (key: string) => string } and a useLocale() hook with the safe-consumer pattern. Pass a small translations object as a prop to the Provider. Use it in two unrelated components to confirm both pull from the same source. Try removing the Provider and verify your custom hook throws a clear error.
Hint
Inside the Provider: const value = useMemo(() => ({ locale, t: (key) => translations[locale]?.[key] ?? key }), [locale, translations]);. The fallback ?? key shows the key itself if no translation exists.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.