Skip to main content

Context

useContext Explained

Written by Published

Context is for values that many components at different depths need - a theme, the current user, a language. It is not a state manager.

The problem it solves

Passing a value through four components that do not use it is called prop drilling. Every layer has to declare and forward a prop it does not care about, and adding a fifth means touching all of them.

The three pieces

  1. createContext(defaultValue) makes the channel.
  2. <Context.Provider value={...}> supplies a value to everything inside it.
  3. useContext(Context) reads the nearest provider's value.

Skipping the middle layers

jsx

const ThemeContext = React.createContext("light")

function DeepButton() {
  // Reads the theme without any component in between passing it down.
  const theme = React.useContext(ThemeContext)
  const dark = theme === "dark"

  return (
    <button style={{
      background: dark ? "#0f172a" : "#e2e8f0",
      color: dark ? "#ffffff" : "#0f172a",
      border: 0, borderRadius: 6, padding: "6px 12px",
    }}>
      Theme is {theme}
    </button>
  )
}

function Middle() {
  // Knows nothing about the theme.
  return <div style={{ padding: 10 }}><DeepButton /></div>
}

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

  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        Toggle theme
      </button>
      <Middle />
    </ThemeContext.Provider>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<App />)

Middle has no props at all, yet the button below it follows the theme. That is the entire point.

The default value

The argument to createContext is used only when a component reads the context with no provider above it. It is a fallback, not an initial value, and a sensible one makes components usable in isolation.

Passing state down

Context carries any value, including a setter. Providing an object with both the value and a way to change it is the usual shape.

Value and updater together

jsx

const CountContext = React.createContext(null)

function Display() {
  const { count } = React.useContext(CountContext)
  return <p>Count is {count}</p>
}

function Controls() {
  const { setCount } = React.useContext(CountContext)
  return (
    <div style={{ display: "flex", gap: 6 }}>
      <button onClick={() => setCount((c) => c - 1)}></button>
      <button onClick={() => setCount((c) => c + 1)}>+</button>
    </div>
  )
}

function App() {
  const [count, setCount] = React.useState(0)
  return (
    <CountContext.Provider value={{ count, setCount }}>
      <Display />
      <Controls />
    </CountContext.Provider>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<App />)

When not to use it

  • Passing down one or two levels. Props are clearer; context hides where a value came from.
  • Frequently changing values. Every consumer re-renders on every change - a value updating per keystroke will be felt.
  • As a state manager. Context distributes a value; it does not manage one. The state still lives in a component above.

The honest rule: reach for context when prop drilling has become genuinely painful, not in anticipation of it. Three levels of props is not painful.