Chapter 13 of 14 · browse all
Chapter 13

Context and useContext

Sharing values across a whole tree without threading props through every level.

The problem: prop drilling

Passing a prop through components that do not use it, purely to reach something deeper, is called prop drilling. With one or two levels it is fine. With five it becomes noise, and every new prop means editing every file in the chain.

<App theme={theme}>
  <Layout theme={theme}>
    <Sidebar theme={theme}>
      <Nav theme={theme}>
        <NavButton theme={theme} />

Creating a context

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

const ThemeContext = createContext("light");

The argument is the default value, used only when a component reads the context with no matching provider above it.

Providing a value

function App() {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Layout />
    </ThemeContext.Provider>
  );
}

Everything rendered inside the provider can read that value, at any depth, without it being passed down.

Reading a value

function ThemeToggle() {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
      Switch to {theme === "light" ? "dark" : "light"} theme
    </button>
  );
}

useContext finds the nearest provider above the component and returns its value. When that value changes, every component reading the context re-renders.

A custom hook to wrap it

Exporting a hook alongside the provider keeps the context object private and gives a clear error when it is used in the wrong place.

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

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

For that check to work, create the context with no default: createContext(undefined).

Nesting and overriding

Providers can be nested, and the nearest one wins. This makes it easy to override a value for one part of the tree.

<ThemeContext.Provider value={{ theme: "light" }}>
  <Page />
  <ThemeContext.Provider value={{ theme: "dark" }}>
    <Footer />   {/* reads dark */}
  </ThemeContext.Provider>
</ThemeContext.Provider>

When to use context

Context suits values that many components need and that change rarely: the current theme, the signed-in user, the interface language, a routing location.

It is not a state manager, and it is not a performance optimisation. Every consumer re-renders when the value changes, so a context holding fast-changing data can cause a lot of unnecessary work. Split one large context into several focused ones rather than putting everything in a single object.

A new object literal as the provider value creates a new reference on every render, so all consumers re-render even when nothing meaningful changed. Wrap it in useMemo when the provider re-renders often.

const value = useMemo(() => ({ theme, setTheme }), [theme]);

return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;