Skip to content

Context

Context lets a parent make a value available to every component below it, without passing props through the layers in between. You create a context, wrap a subtree in a provider, and any descendant reads it with useContext.

import { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
function App() {
const [theme, setTheme] = useState("dark");
return (
// React 19: you can render <ThemeContext> directly as the provider.
// (The classic <ThemeContext.Provider> still works too.)
<ThemeContext value={theme}>
<Page />
</ThemeContext>
);
}
function Button() {
const theme = useContext(ThemeContext); // reads the nearest provider's value
return <button className={theme}>Click</button>;
}

Button gets theme no matter how deep it is, and no intermediate component has to forward it. This is the fix for prop drilling when composition can’t reach the consumers.

Context is designed for low-frequency, broadly-shared values: theme, current user/auth, locale, a design-system config. Things that many components read but that change rarely.

The catch is performance: when a provider’s value changes, every consumer re-renders — and React can’t skip them with memo, because they subscribe to the context directly.

flowchart TB
  prov["Provider value changes"] --> c1["Consumer A re-renders"]
  prov --> c2["Consumer B re-renders"]
  prov --> c3["Consumer C re-renders"]
  note["memo cannot stop a context re-render"] -.-> c1
A context value change re-renders all its consumers

So putting fast-changing state (e.g. a value that updates on every keystroke or animation frame) in a single wide context is a performance trap — it re-renders the whole subtree constantly.

Two mitigations:

  • Split contexts. Put unrelated values in separate providers so a change to one doesn’t re-render consumers of the other. A common split: one context for the state and one for the dispatch/updater (the updater is stable, so components that only dispatch never re-render).
  • Keep the value stable. Memoize the provider’s value object so it doesn’t get a new reference on every parent render.

A crucial distinction: Context is a transport mechanism, not a state container. It moves a value down the tree; it does not hold or update state. The state itself still lives in a useState or useReducer in the provider component. “Using Context for state management” really means “useState/useReducer at the top, delivered by Context.”

function AuthProvider({ children }) {
const [user, setUser] = useState(null); // the state lives here
return <AuthContext value={{ user, setUser }}>{children}</AuthContext>; // Context just delivers it
}
What problem does Context solve?
What happens to consumers when a context provider's value changes?
Why is Context "not a state manager"?
What is a good mitigation for context re-render performance?