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.

Before React 19, the only way to provide a context value was <ThemeContext.Provider value={...}>. React 19 lets you render the context object itself as a component — <ThemeContext value={...}> — and it behaves as the provider. The createContext reference says it plainly:

Starting in React 19, you can render <SomeContext> as a provider. In older versions of React, use <SomeContext.Provider>.

.Provider isn’t removed — both forms provide the same context — but new code should reach for the shorter form first.

src/components/ThemeApp.tsx
import { createContext, useContext, useState } from 'react';
export const ThemeContext = createContext('light');
export function ThemeApp() {
const [theme, setTheme] = useState('dark');
return (
// React 19: <ThemeContext> itself is the provider — no `.Provider`.
<ThemeContext value={theme}>
<ThemeLabel />
<button onClick={() => setTheme((t) => (t === 'dark' ? 'light' : 'dark'))}>
toggle
</button>
</ThemeContext>
);
}
function ThemeLabel() {
const theme = useContext(ThemeContext);
return <span>{theme}</span>;
}
src/__tests__/context-direct-provider.test.tsx
import { render, screen, cleanup } from '@testing-library/react';
import { afterEach, expect, test } from 'vitest';
import { ThemeApp } from '../components/ThemeApp';
afterEach(cleanup);
test('React 19 renders <Context> directly as its own provider — no .Provider needed', () => {
render(<ThemeApp />);
expect(screen.getByText('dark')).toBeInTheDocument();
});
✓ React 19 renders <Context> directly as its own provider — no .Provider needed
Test Files 1 passed (1)
Tests 1 passed (1)

use(Context): reading context conditionally

Section titled “use(Context): reading context conditionally”

useContext must be called unconditionally at a component’s top level, like every Hook. use reads a context the same way, but the use reference is explicit that it drops that restriction:

Unlike useContext, use can be called within loops and conditional statements like if.

That matters for a component that only needs the context value along one branch — with useContext you’d read it unconditionally (and either not use it, or restructure the component); with use you read it exactly where it’s needed.

src/components/ConditionalThemeReader.tsx
import { createContext, use } from 'react';
export const LabelContext = createContext('light');
export function ConditionalThemeReader({ show }: { show: boolean }) {
let label = 'hidden';
if (show) {
// Unlike useContext, `use` is allowed inside a conditional block.
label = use(LabelContext);
}
return <span>{label}</span>;
}
src/__tests__/use-context-conditional.test.tsx
import { render, screen, cleanup } from '@testing-library/react';
import { afterEach, expect, test } from 'vitest';
import { ConditionalThemeReader, LabelContext } from '../components/ConditionalThemeReader';
afterEach(cleanup);
test('use(Context) reads the value when the conditional branch runs', () => {
render(
<LabelContext value="dark">
<ConditionalThemeReader show={true} />
</LabelContext>,
);
expect(screen.getByText('dark')).toBeInTheDocument();
});
test('use(Context) is simply never called when the branch does not run', () => {
render(
<LabelContext value="dark">
<ConditionalThemeReader show={false} />
</LabelContext>,
);
expect(screen.getByText('hidden')).toBeInTheDocument();
});
✓ use(Context) reads the value when the conditional branch runs
✓ use(Context) is simply never called when the branch does not run
Test Files 1 passed (1)
Tests 2 passed (2)

use still searches upward for the nearest provider above the calling component, exactly like useContext — it just isn’t pinned to the top of the function.

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. The useContext reference states this as a caveat:

React automatically re-renders all the children that use a particular context starting from the provider that receives a different value. […] Skipping re-renders with memo does not prevent the children receiving fresh context values.

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.

Proof: a new provider value re-renders a memoized consumer every time

Section titled “Proof: a new provider value re-renders a memoized consumer every time”

The trap is subtle because it doesn’t require the data to change — a brand-new object literal with the same fields is enough, because React compares the provider’s value by reference (Object.is), not by deep equality.

src/components/LabelProvider.tsx
import { createContext, useContext, useState, useMemo, memo } from 'react';
type Ctx = { label: string };
export const LabelContext = createContext<Ctx>({ label: '' });
export const Consumer = memo(function Consumer({ onRender }: { onRender: () => void }) {
const { label } = useContext(LabelContext);
onRender();
return <span>{label}</span>;
});
export function UnstableValueApp({ onChildRender }: { onChildRender: () => void }) {
const [tick, setTick] = useState(0);
// ❌ A brand-new object every render — even though `label` never changes.
return (
<LabelContext value={{ label: 'fixed' }}>
<Consumer onRender={onChildRender} />
<button onClick={() => setTick((t) => t + 1)}>{tick}</button>
</LabelContext>
);
}
export function StableValueApp({ onChildRender }: { onChildRender: () => void }) {
const [tick, setTick] = useState(0);
// ✅ Memoized — same reference across renders, so memo can bail out.
const value = useMemo(() => ({ label: 'fixed' }), []);
return (
<LabelContext value={value}>
<Consumer onRender={onChildRender} />
<button onClick={() => setTick((t) => t + 1)}>{tick}</button>
</LabelContext>
);
}
src/__tests__/context-value-identity.test.tsx
import { render, screen, cleanup, act } from '@testing-library/react';
import { afterEach, expect, test } from 'vitest';
import { UnstableValueApp, StableValueApp } from '../components/LabelProvider';
afterEach(cleanup);
test('an inline provider value re-renders a memoized consumer on every unrelated update', () => {
let renders = 0;
render(<UnstableValueApp onChildRender={() => renders++} />);
const baseline = renders;
act(() => { screen.getByRole('button').click(); });
act(() => { screen.getByRole('button').click(); });
// Two unrelated parent re-renders (the tick counter) still re-rendered the
// memoized consumer twice, because it got a brand-new context value object
// both times — memo cannot see past the reference change.
expect(renders - baseline).toBe(2);
});
test("a useMemo'd provider value lets memo bail the consumer out", () => {
let renders = 0;
render(<StableValueApp onChildRender={() => renders++} />);
const baseline = renders;
act(() => { screen.getByRole('button').click(); });
act(() => { screen.getByRole('button').click(); });
// Same two clicks, but the context value's reference never changes, so
// memo bails the consumer out of both re-renders.
expect(renders - baseline).toBe(0);
});
✓ an inline provider value re-renders a memoized consumer on every unrelated update
✓ a useMemo'd provider value lets memo bail the consumer out
Test Files 1 passed (1)
Tests 2 passed (2)
import { createContext, useContext, useState, useMemo, memo } from 'react';
const LabelContext = createContext({ label: 'fixed' });
const Consumer = memo(function Consumer() {
const { label } = useContext(LabelContext);
return <li>Consumer reads: {label}</li>;
});
export default function App() {
const [tick, setTick] = useState(0);
const [stable, setStable] = useState(false);
const stableValue = useMemo(() => ({ label: 'fixed' }), []);
const value = stable ? stableValue : { label: 'fixed' };
return (
<LabelContext value={value}>
<button onClick={() => setTick((t) => t + 1)}>Unrelated tick: {tick}</button>
<button onClick={() => setStable((s) => !s)}>
Provider value: {stable ? 'useMemo (stable)' : 'inline object (fresh every render)'}
</button>
<ul>
<Consumer />
</ul>
</LabelContext>
);
}
Babel 7.29.9 (jsdelivr) · React 19.3.0 dev build (esm.sh, pinned)
ComponentRendersLast commit (ms)
Click Run — commits will appear here.

With the mode reading “inline object”, click Unrelated tick a few times — Consumer’s row climbs on every click, even though label itself never changes, because memo cannot see past the provider’s fresh object reference (proven above). Switch the mode to “useMemo (stable)” and click Unrelated tick again — Consumer’s row stops moving entirely, because the provider now hands out the same object reference every time.

Two mitigations for this class of bug:

  • 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) — the useReducer lesson builds this pattern out in full.
  • Keep the value stable. Memoize the provider’s value object (useMemo) so it doesn’t get a new reference on every parent render, exactly as proven above.

Splitting contexts is a manual, static fix — you decide at design time which fields live in which provider. What Context cannot do is let one consumer dynamically subscribe to “just field x of this object” while another subscribes to “just field y” from the same context: any change to the provider’s value re-renders every consumer of that context, full stop, regardless of which field a given consumer actually reads.

An external store’s selector (useStore(s => s.x)) re-renders a component only when the selected slice changes, comparing that narrower value instead of the whole store. Context has no equivalent — it compares the whole value reference. When an app has many consumers each caring about a different narrow slice of a large, frequently-changing value, that’s the signal to reach past Context for an external store (the next lesson).

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
}

Think of a context as a radio broadcast, not a mailbox: the provider transmits one value, and every component tuned to that station (every useContext/use call for that context, anywhere below) receives the same broadcast at the same time, whether it wanted this particular update or not. There is no way to broadcast on sub-frequencies within one station — that’s what “no selectors” means. If different listeners need different update frequencies, you need separate stations (split contexts) or a receiver built for exactly that (an external store’s selector).

ChoiceBenefitCost
<Context> direct provider (React 19)Shorter, no .Provider to rememberRequires React 19+; .Provider still needed on older versions
use(Context)Can be called conditionally/in loopsSlightly less familiar than useContext; same upward-search caveats apply
Single wide contextSimple to set upAny value change re-renders every consumer, memo cannot help
Split contexts (state vs dispatch)Dispatch-only consumers never re-renderMore providers to wire up
useMemo’d provider valuePrevents re-renders from a fresh object literalOne more dependency array to keep correct
External store (next lesson)True per-slice selectorsOutside React; another API to learn
  • “memo on a consumer protects it from context updates.” It doesn’t — memo only checks props. A context value change re-renders every consumer regardless of memo, as react.dev states directly and as proven above.
  • “If the data inside the value object didn’t change, the consumer won’t re-render.” It will, if the value is a new object literal — React compares the provider’s value by reference, not by deep equality.
  • “Splitting contexts is the same as selectors.” Splitting is a manual, compile-time decision about which fields go where. A selector dynamically picks a slice of a single value at read time — Context has no such mechanism.
  • “Context replaces useState/useReducer.” It only transports whatever useState/useReducer already computed — the state still lives in the provider component.

When you review a generated Provider component, check for these before trusting it:

  • Does the provider’s JSX construct the value prop inline (value={{ ... }} or value={[a, b]}) instead of passing a variable held in useMemo/useState? That’s a new reference every render, and every consumer re-renders — even ones wrapped in memo.
  • Is a single context carrying both slow-changing data (like a logged-in user) and fast-changing data (like a live cursor position or a form’s every keystroke)? That belongs split into two contexts, or moved to an external store.
  • Does the code assume wrapping a consumer in memo will stop it from re-rendering on a context change? It won’t — verify by checking whether the provider’s value is stable, not by adding memo to the consumer.
  • Is use(Context) called somewhere useContext would also work fine (an unconditional top-level call)? Either is correct there — but if you see use specifically inside an if/loop, that’s why it’s use and not useContext, not a mistake.
  • Does the component read the whole context value just to display one field, when a useReducer+split-context (or external-store selector) would isolate it from unrelated updates?

🐛 Spot the bug

function CartProvider({ children }) {
const [items, setItems] = useState([]);
const [user, setUser] = useState(null);
return (
<CartContext
value={{
items,
user,
addItem: (item) => setItems((prev) => [...prev, item]),
}}
>
{children}
</CartContext>
);
}
Show the answer

The value object is built inline on every render of CartProvider — a brand-new object, even when neither items nor user actually changed. Every consumer of CartContext re-renders whenever CartProvider re-renders for any reason (say, an unrelated parent state update), and memo on those consumers cannot stop it, exactly as proven above with UnstableValueApp. The fix is useMemo(() => ({ items, user, addItem }), [items, user, addItem]) (with addItem itself wrapped in useCallback) — now the value only gets a new reference when items or user actually changes.

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?
What can use(Context) do that useContext(Context) cannot?
Clicking the button twice — how many times does a memo-wrapped Consumer re-render?
const value = useMemo(() => ({ label: 'fixed' }), []);
return (
  <LabelContext value={value}>
    <Consumer onRender={onChildRender} />
    <button onClick={() => setTick((t) => t + 1)}>{tick}</button>
  </LabelContext>
);
Same click, but the value is now an inline object literal built every render. How many times does the memoized Consumer re-render for two clicks?
return (
  <LabelContext value={{ label: 'fixed' }}>
    <Consumer onRender={onChildRender} />
    <button onClick={() => setTick((t) => t + 1)}>{tick}</button>
  </LabelContext>
);