useContext

Share Data Without Threading Props Through Every Layer

useContext

Context lets a deep child read a value from any ancestor without intermediate components having to pass it through.

5 min read Level 2/5 #react#useContext#context
What you'll learn
  • Create a context with `createContext`
  • Provide a value with `<Context.Provider>`
  • Read it from any descendant with `useContext`

When the same value is needed in many components — a theme, the current user, a translator function — passing it through every layer as a prop gets painful. Context is the React way to skip that.

Three Pieces

  1. Create a context
  2. Wrap part of the tree in <Context.Provider value={...}>
  3. Read the value with useContext from any descendant

Step By Step

// 1. Create
const ThemeContext = createContext("light");

// 2. Provide
function App() {
  const [theme, setTheme] = useState("dark");
  return (
    <ThemeContext.Provider value={theme}>
      <Page />
    </ThemeContext.Provider>
  );
}

// 3. Read
function Button() {
  const theme = useContext(ThemeContext);
  return <button className={`btn btn--${theme}`}>OK</button>;
}

Button reads theme even though App never passed it as a prop to anyone — context skipped the layers.

Defaults

The argument to createContext is the value seen by consumers that are NOT wrapped in a provider:

const ThemeContext = createContext("light");

// no provider above:
useContext(ThemeContext);   // "light"

A real default makes a component usable in isolation (tests, stories) without setting up a provider just to render it.

Passing Setters Too

Context can carry a full object — including functions to update state.

const ThemeContext = createContext(null);

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");
  const value = { theme, setTheme };
  return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}

function ThemeToggle() {
  const { theme, setTheme } = useContext(ThemeContext);
  return (
    <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
      Toggle
    </button>
  );
}

Wrap the raw context with a custom hook + a provider component. Consumers shouldn’t see the raw context at all.

const ThemeContext = createContext(null);

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme must be used inside ThemeProvider");
  return ctx;
}

Now in components: const { theme, setTheme } = useTheme();. No imports of the raw context anywhere.

Performance Note

When the provider’s value changes, every consumer re-renders. That means every component using useContext for this context. For lightweight values that change rarely (theme, locale) this is fine. For frequently-changing state shared with many components, consider splitting into multiple contexts or using a dedicated state library.

When To Use Context

Good fits:

  • Theme, locale, current user
  • A dependency-injected service (logger, analytics)
  • “Slot” patterns inside complex components

Not great:

  • General app state — a state library (Zustand, Redux) is better at scale
  • Frequently changing values used by many components
  • Things only one component needs — just pass a prop

Up Next

When state logic gets complex, useReducer is the upgrade from useState.

useReducer →