Must knowAdvanced React APIs · 02

Context with use

createContext, a provider and use(): skip the prop drilling, and know exactly which components re-render.

On this page · 7 sections
  1. In one minute
  2. The example: theme and cart
  3. How it works
  4. Common mistakes
  5. Interview Q&A
  6. Related
  7. Sources

In one minute

Prop drilling means passing a value through layers of components that don’t use it, only to reach one that does. Context solves it in three steps:

  1. createContext(defaultValue) declares something that can be shared.
  2. <MyContext value={…}> provides a value to everything below it.
  3. Any component below reads it with use(MyContext).

When the provided value changes, React re-renders the components that read it, even if the components in between are memoized. It decides “changed” with Object.is, so a provider value written as a new object on every render counts as a change on every render.

You’ll be able to: share a theme and a cart through context, give consumers a clear error when the provider is missing, and keep context from re-rendering more than it should.

Theme, memoized value
Store.tsx
1const ThemeContext = createContext('light')2const CartContext = createContext(null)34function CartProvider({ children }) {5  const [count, setCount] = useState(0)6  const value = useMemo(() => ({ count, add }), [count])7  return <CartContext value={value}>{children}</CartContext>8}910function ProductCard({ name }) {11  const theme = use(ThemeContext)12  return <article className={theme}>{name}</article>13}1415function CartBadge() {16  const { count } = use(CartContext)17  return <span>{count} in cart</span>18}
What you see
Theme: light
0 in cart
Add to cart
Rendered this step
App–
CartProvider–
Header (memo)–
ProductGrid (memo)–
ProductCard ×3–
CartBadge–
AddButton–
  1. eventClick the theme toggle. The theme state lives in App, so App renders and passes "dark" to ThemeContext.
  2. render phaseconst value = useMemo(() => ({ count, add }), [count])CartProvider is a child of App, so it renders too. Its useMemo returns the same value object, because count didn’t change.
  3. render phaseconst theme = use(ThemeContext)Header and ProductGrid are memoized and got no new props, so they’re skipped. React doesn’t need them: it finds the components that read the changed context and renders those directly.
  4. commit phaseRecorded: App, CartProvider and the three cards. The cart readers didn’t render: their context didn’t change.

0 / 0
A store with two contexts: the theme (read by the product cards) and the cart (read by the badge and the Add button). Header and ProductGrid sit in between and are memoized (all three runs recorded).

The example: theme and cart

The store has two shared values. The theme is read by every product card. The cart is read by the header badge and the “Add to cart” button. Without context, App would pass theme through ProductGrid, and the cart through Header and ProductGrid, though neither of them uses it.

1. Create the contexts

type Theme = 'light' | 'dark'
const ThemeContext = createContext<Theme>('light') // 'light' is used when no provider is above
type CartValue = { count: number; add: () => void }
const CartContext = createContext<CartValue | null>(null)

function useCart() {
	const value = use(CartContext)
	if (value === null) throw new Error('useCart must be used inside <CartProvider>')
	return value
}

CartContext defaults to null, meaning “no provider”. The useCart hook turns that into a clear error and narrows the type from CartValue | null to CartValue for every caller.

2. Provide the values

function CartProvider({ children }: { children: ReactNode }) {
	const [count, setCount] = useState(0)
	const value = useMemo(() => ({ count, add: () => setCount((c) => c + 1) }), [count])
	log('render CartProvider')
	return <CartContext value={value}>{children}</CartContext>
}
function App({ Provider = CartProvider }) {
	const [theme, setTheme] = useState<Theme>('light')
	log('render App')
	return (
		<ThemeContext value={theme}>
			<Provider>
				<button id="theme" onClick={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))}>
					Toggle theme
				</button>
				<Header />
				<ProductGrid />
			</Provider>
		</ThemeContext>
	)
}

Since React 19 you render the context itself as the provider: <ThemeContext value={…}>. The older <ThemeContext.Provider value={…}> still works.

3. Read them

function CartBadge() {
	const { count } = useCart()
	log('render CartBadge')
	return <span id="badge">{count} in cart</span>
}

function ProductCard({ name, onSale }: { name: string; onSale: boolean }) {
	const theme = use(ThemeContext)
	log(`render ProductCard (${name})`)
	return (
		<article className={`product-card ${theme}`}>
			<h2>{name}</h2>
			<SaleTag onSale={onSale} />
		</article>
	)
}

function SaleTag({ onSale }: { onSale: boolean }) {
	if (!onSale) return null
	const theme = use(ThemeContext) // use() may be called after an early return
	return <span className={`tag tag--sale ${theme}`}>Sale</span>
}

SaleTag calls use after an early return. That’s allowed for use and not for any hook, including useContext.

Who re-rendered

Toggling the theme:

render App
render CartProvider
render ProductCard (Ceramic Mug)
render ProductCard (Wireless Headphones)
render ProductCard (Trail Backpack)

Header and ProductGrid are memo components with no changed props, so they were skipped. React still reached the three ProductCards below ProductGrid, because they read the theme. CartBadge and AddButton didn’t render: the cart value is memoized and didn’t change.

Clicking “Add to cart”:

render CartProvider
render CartBadge
render AddButton

Now the same toggle with a provider that builds its value inline:

const value = { count, add: () => setCount((c) => c + 1) } // a new object every render
render App
render CartProvider
render CartBadge
render ProductCard (Ceramic Mug)
render ProductCard (Wireless Headphones)
render ProductCard (Trail Backpack)
render AddButton

CartBadge and AddButton rendered too. The theme toggle re-rendered CartProvider, which built a new object with the same contents, and React treats a new object as a new value.

No provider

<ProductCard name="Desk Lamp" onSale />
<ErrorBoundary fallbackRender={({ error }) => <p role="alert">{(error as Error).message}</p>}>
	<CartBadge />
</ErrorBoundary>

What rendered:

{
  "logs": [
    "render ProductCard (Desk Lamp)",
    "render ProductCard (Desk Lamp)"
  ],
  "warnings": [
    "Error: useCart must be used inside <CartProvider>"
  ],
  "article": "product-card light",
  "sale": "tag tag--sale light",
  "alert": "useCart must be used inside <CartProvider>"
}

The card fell back to ThemeContext’s default, 'light'. The badge hit useCart’s check, and the error boundary showed the message instead of a confusing Cannot read properties of null.

How it works

Common mistakes

Interview Q&A

What problem does context solve?

Prop drilling: passing a value through components that don’t use it so a deep descendant can. With context, any component below a provider reads the value directly.

Walk through the three pieces of the context API.

createContext(default) creates the context. <Ctx value={v}> (React 19; <Ctx.Provider> before that) provides a value to its subtree. use(Ctx) or useContext(Ctx) reads the nearest provider’s value, or the default if there’s none.

What's the difference between use and useContext?

They read context the same way. use isn’t bound by the rules of hooks, so it can run inside conditions, loops or after an early return, and it can also unwrap a promise. useContext must be called at the top level every render and only reads context.

When a context value changes, which components re-render?

Every component that reads that context, wherever it is below the provider, even inside memoized parents. Recorded: toggling the theme rendered the three ProductCards, while the memoized Header and ProductGrid were skipped.

Why can an inline provider value hurt performance?

Context changes are detected with Object.is, and { count, add } is a new object on every render. Recorded: with the inline value, a theme toggle also rendered CartBadge and AddButton, although the cart hadn’t changed. useMemo fixed it.

How do you make a missing provider fail loudly?

Default the context to null and read it through a custom hook that throws if the value is null. Recorded: “useCart must be used inside <CartProvider>”, shown by the error boundary.

When is context the wrong tool?

For values that change very often and are read by many components (every reader re-renders each time). Use a store with selectors, or split the context.

Sources

Primary source: react.dev/reference/react/createContext