Context

React · Practical · 12 min

Themes, the current user, locale and similar app-wide values need to reach distant components cleanly.

Passing props through many layers that don't use them (prop drilling) gets tedious. Context lets a parent make a value available to every component below it, however deep.

Three steps:

const ThemeContext = createContext("light");          // 1. create, with a default
<ThemeContext value="dark"><Page /></ThemeContext>     // 2. provide
const theme = useContext(ThemeContext);                // 3. read

In React 19 you render the context itself as the provider (<ThemeContext value=...>); <ThemeContext.Provider> still works but is on its way out. A component reads the value from the nearest provider above it; with none, it gets the default from createContext. Providers can be nested to override values for a subtree.

React 19's use(ThemeContext) reads context too, and unlike useContext it can be called inside conditions and loops.

When the provided value changes, every component reading that context re-renders. Two consequences:

  • If you provide an object, value={{ user, setUser }} creates a new object each render, re-rendering all consumers. Memoize it with useMemo (or let the React Compiler do it), or split it into separate contexts for rarely and frequently changing parts.
  • Context isn't a substitute for a state manager with fine-grained subscriptions; for large, fast-changing shared state consider an external store.

A very common pattern is context plus a reducer: keep state in a top-level component with useReducer and provide both state and dispatch via context, wrapped in a custom Hook like useTasks().

Before reaching for context, try composition: passing JSX as children often removes the drilling with no new concepts.

Example

import { createContext, use, useContext, useState } from "react";

const ThemeContext = createContext("light");
const UserContext = createContext(null);

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

function Greeting({ show }) {
  if (!show) return null;
  const user = use(UserContext);     // use() may follow an early return
  return <p>Signed in as {user.name}</p>;
}

export default function App() {
  const [theme, setTheme] = useState("light");
  return (
    <UserContext value={{ name: "Ada" }}>
      <ThemeContext value={theme}>
        <Greeting show />
        <Button>Save</Button>
        <ThemeContext value="dark">
          <Button>Always dark</Button>
        </ThemeContext>
        <button onClick={() => setTheme(t => (t === "light" ? "dark" : "light"))}>Toggle</button>
      </ThemeContext>
    </UserContext>
  );
}

Remember

  • createContext(default), provide with <Ctx value={...}>, read with useContext(Ctx) or use(Ctx)
  • Consumers read the nearest provider above them and re-render when its value changes
  • Keep provided objects stable and split contexts by update frequency

Watch out for

  • Calling useContext in the same component that renders the provider: it reads the provider above, not its own.

Check yourself

  1. A component calls useContext(ThemeContext) but no ThemeContext provider exists above it. What does it get?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: react.dev/learn/passing-data-deeply-with-context

Builds on

Unlocks

Nothing builds on this yet.