Topic 9.3
Context Patterns and Pitfalls
In one line
Context fits values many distant components read and that change rarely, such as the theme, the signed-in user and the locale; every reader re-renders whenever the value changes, so keep the value stable with useMemo/useCallback, split values that change at different rates into separate contexts, and don't use context for fast-changing or server data.
Think of it like this
A school's loudspeaker. It's perfect for "school closes early today": everyone needs it, and it happens rarely. But if the office used the loudspeaker to announce every single page a teacher turns, every class would stop and listen all day long. Context is a loudspeaker: great for rare, school-wide news, terrible for frequent, small updates that only one room cares about.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Reader (consumer)
- A component that reads a context with
useContext(oruse). - Object.is
- The comparison React uses to decide if a value changed. Two different objects are never equal, even with the same contents.
- memo
- A wrapper that lets a component skip rendering when its props haven't changed (Topic 16.3).
- useMemo
- A hook that remembers a calculated value and returns the same one until its dependencies change.
- useCallback
- A hook that returns the same function on every render until its dependencies change.
- Splitting contexts
- Putting values that change at different rates into separate contexts so readers only re-render for what they use.
- Provider component
- Your own component that owns the state and renders the context provider around
children. - Locale
- A user's language and region, like
en-IN, which decides how numbers, dates and currency are written. - Selector
- A function that picks the small part of a store a component needs, so it re-renders only when that part changes.
Step by step
01Choose what belongs in context
Ask two questions: do many components far apart need it, and does it change rarely? Theme, user and locale pass both tests. A search box's text fails the first; the mouse position fails the second.
02Spot the new-object trap
This provider looks harmless, but { theme, toggle } and the arrow function are new on every render of App. Any state change in App gives every reader a "new" value.
function App() {
const [theme, setTheme] = useState("light");
const [clicks, setClicks] = useState(0); // unrelated state
return (
<ThemeContext value={{ theme, toggle: () => setTheme(flip) }}>
{/* every reader re-renders when clicks changes */}
</ThemeContext>
);
}03Fix 1: memoise the value
Keep the function stable with useCallback and the object stable with useMemo. The value is now a new object only when theme changes.
const toggle = useCallback(() => setTheme((t) => (t === "light" ? "dark" : "light")), []);
const value = useMemo(() => ({ theme, toggle }), [theme, toggle]);
return <ThemeContext value={value}>{children}</ThemeContext>;04Fix 2: split what changes from what doesn't
The theme string changes; toggle never does. Two contexts let a component that only toggles ignore theme changes.
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const toggle = useCallback(() => setTheme((t) => (t === "light" ? "dark" : "light")), []);
return (
<ThemeContext value={theme}>
<ToggleThemeContext value={toggle}>{children}</ToggleThemeContext>
</ThemeContext>
);
}05Group providers in one place
Real apps stack several providers. Put them in one AppProviders component so main.jsx stays readable and tests can reuse it as a wrapper (Topic 17.6).
export function AppProviders({ children }) {
return (
<ThemeProvider>
<UserProvider>
<LocaleProvider>{children}</LocaleProvider>
</UserProvider>
</ThemeProvider>
);
}Try it yourself
- 1
Predict before you fix
In the trap example, change
Sidebarto readuseContext(ThemeContext)(and ignore the result). Predict the logs after the unrelated click. (Sidebar renderedjoins the list: reading a context is enough to be re-rendered by it, even if you don't use the value.) - 2
Take memo away
In Fix 2, remove
memo(...)fromSidebar(write it as a plain function). Predict the logs for the unrelated click. (Sidebarrenders again: withoutmemo, a child renders whenever its parent does. Context andmemoare separate reasons to render; Topic 16.1 lists them all.) - 3
Turn App into a provider component
In Fix 2, move the theme state,
toggleand both providers into aThemeProvider({ children })component, and render<ThemeProvider><Header /><Sidebar /><ToggleButton /></ThemeProvider>fromApp. Remove allmemocalls. Predict what renders on toggle. (OnlyThemeProviderandHeader: the children were created byApp, which didn't render.)
Code & diagrams
Each value comes straight from state, so it keeps the same identity until it really changes: no memoising needed. The script switches the locale once; `Intl.NumberFormat` groups Indian numbers in lakhs and crores.
Preview
Console
Nothing logged yet.Expected console output
en-IN ₹12,34,567.00
en-US $1,234,567.00The unrelated click has nothing to do with the theme, yet `Header` and `ToggleButton` render again, straight through `memo`. `Sidebar` reads no context, so `memo` skips it.
Preview
Console
Nothing logged yet.Expected console output
App rendered
Header rendered
Sidebar rendered (reads no context)
ToggleButton rendered
--- click the unrelated button
App rendered
Header rendered
ToggleButton rendered
--- click Toggle theme
App rendered
Header rendered
ToggleButton renderedThe unrelated click now renders only `App`: the context value is the same object, so no reader is disturbed. Toggling still renders both readers, because the object really changed, and `ToggleButton` reads it even though it only needs `toggle`.
Preview
Console
Nothing logged yet.Expected console output
App rendered
Header rendered
Sidebar rendered (reads no context)
ToggleButton rendered
--- click the unrelated button
App rendered
--- click Toggle theme
App rendered
Header rendered
ToggleButton renderedNow toggling renders only `Header`, the one component that shows the theme. `ToggleButton` reads a context whose value (`toggle`) never changes. The theme string needs no `useMemo`: a string is compared by value.
Preview
Console
Nothing logged yet.Expected console output
App rendered
Header rendered
Sidebar rendered (reads no context)
ToggleButton rendered
--- click the unrelated button
App rendered
--- click Toggle theme
App rendered
Header renderedBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Forgetting a dependency in useMemo
In Fix 1, change the value to useMemo(() => ({ theme, toggle }), []) (empty dependency list) and click Toggle theme a few times.
Break #2
Putting a fast-changing value in context
Store the mouse position in state at the top of the app and provide it: <MouseContext value={{ x, y }}>, updated on every pointermove.
Myth vs fact
Myth
Wrapping readers in memo stops context re-renders.
Fact
memo only compares props. A context change renders every reader, even inside a memo component.
Myth
A component re-renders only if it uses the part of the context that changed.
Fact
React can't see which fields you use. Any change to the value (a new object) renders every reader of that context.
Myth
You must always wrap context values in useMemo.
Fact
Only objects, arrays and functions created during render need it. Strings, numbers and state values are already stable. And if the provider's component rarely renders for other reasons, the gain is small.
Myth
Context replaces Redux or Zustand.
Fact
Context passes a value down; it has no selectors, middleware or devtools. It's enough for small and rarely changing global state. Large, often-changing state is usually better in a store (Phase 15).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
React compares context values with
Object.isand, when the value changes, walks the provider's subtree to find fibers whose context dependency list contains this context, then marks them to update. That walk is why a huge tree under a frequently changing provider costs time even when few components read it. - ▸
There have been RFCs for context selectors (
useContextSelector, reading part of a context), and the third-partyuse-context-selectorpackage implements the idea, but React itself has no such API in 19.2. Libraries like React Redux and Zustand put a stable store object in context (or none at all) and useuseSyncExternalStorewith selectors, so context never changes and only components whose slice changed render (Topic 15.2). - ▸
With the React Compiler (Topic 16.5), an inline
value={{ theme, toggle }}is memoised automatically when its inputs don't change, so the new-object trap mostly disappears. Splitting contexts is still useful because the compiler can't make a reader skip a value that really changed. - ▸
Splitting can go too far: ten tiny contexts each with a provider make the tree deep and the code hard to follow. Split by rate of change and who reads it (state vs actions is the common split), not one context per field.
Remember this
- 1
The classic uses. Context shines for values that many components across the app read and that change rarely: the colour theme (light or dark), the current user (who is signed in, their permissions), the locale (language and number formats, used with
Intl.NumberFormatandIntl.DateTimeFormat), plus things like feature flags, a router's location, or a design system's settings. Libraries use it too: React Router, TanStack Query and React Redux all put their objects into context so their hooks can find them. - 2
Rule 1: every reader re-renders when the value changes. When a provider renders with a new
value, React compares it with the previous one usingObject.is(the same comparison as state, Topic 4.2). If it differs, every component under that provider that reads the context renders again. There is no way to read only part of a context: a component that readsuser.namere-renders whenuser.avatarchanges too. And context goes throughmemo:memois a wrapper that skips rendering a component when its props haven't changed (Topic 16.3), but a context change still renders the readers inside it. - 3
Rule 2: a new object is a new value. The most common mistake is writing
<ThemeContext value={{ theme, toggle }}>. The{ ... }creates a new object on every render of the provider's component, and() => ...creates a new function.Object.issees a different object each time, so every reader re-renders whenever the provider's component renders, even for reasons that have nothing to do with the theme. Values that are plain strings, numbers or state objects are fine, because they keep the same identity until they really change. - 4
Fix 1: memoise the value.
useMemo(() => ({ theme, toggle }), [theme, toggle])returns the same object untilthemeortogglechanges, anduseCallback(fn, [])returns the same function on every render (both are covered fully in Topic 16.4; the React Compiler can add them for you, Topic 16.5). Now unrelated renders of the provider's component no longer reach the readers. Fix 2: split the context. Put values that change at different rates in different contexts: thethemestring in one, thetogglefunction (which never changes) in another. A button that only toggles reads only the second context, so it doesn't re-render when the theme changes. The same split, with state anddispatch, is the heart of Topic 9.6. - 5
A provider component helps. Put the state and the provider together in one component that receives
children:function ThemeProvider({ children }) { const [theme, setTheme] = useState("light"); ... return <ThemeContext value={...}>{children}</ThemeContext>; }. When the theme changes, onlyThemeProviderand the context's readers render; thechildrenelements were created by its parent, so the rest of the app is skipped (Topic 9.1). Several providers are often grouped into oneAppProviderscomponent at the top of the app. - 6
When not to use context. Don't use it to avoid passing props one or two levels (pass them). Don't use it for state only one area needs (keep it local or lift it, Topic 6.3). Don't use it for fast-changing values such as the mouse position, scroll position or the text of an input on every keystroke: every reader re-renders each time. Don't use it as a cache for server data (lists of products, the user's orders): a data library such as TanStack Query (Topic 14.6) handles loading, caching and refetching. And for large app-wide state where each component needs a small slice, a store with selectors (Zustand, Redux Toolkit, or your own with
useSyncExternalStore) renders only the components whose slice changed (Topics 10.3 and 15.2).
Explain it without notes
Which values are good candidates for context, and which aren't? Give examples of each.
Why does <Ctx value={{ user, logout }}> cause extra re-renders, and how do you fix it?
What does splitting a context mean and when is it worth it?
Does memo stop a component from re-rendering when a context it reads changes? Explain.
Why do state libraries with selectors render fewer components than context for the same state?
Practice
This provider re-renders every reader whenever App renders: <CartContext value={{ items, add: (i) => setItems([...items, i]) }}>. Fix it with useCallback and useMemo.
Split an AuthContext that holds { user, login, logout } into a state context and an actions context, and write a LogoutButton that doesn't re-render when the user changes.
Trade-offs
- ↔
Memoising the value is a small, local change but adds hook noise and dependency lists to maintain; splitting contexts saves more renders but adds providers and hooks. Start with the value from state, memoise when objects appear, split when profiling shows wasted renders.
- ↔
Context is built in and has no dependencies but no selectors; a store library adds a dependency and a concept but renders only what changed. For a theme or a user, context wins; for a big, busy client state, a store usually does.
Done when you can
I can name good and bad uses of context.
I can explain why every reader re-renders when the value changes, even through
memo.I can spot an inline object or function value and fix it with
useMemoanduseCallback.I can split one context into state and actions contexts.
I can wrap state and providers in a provider component that takes
children.I know when to use a store with selectors instead of context.