Command Palette

Search for a command to run...

Hectal
PHASE 9Intermediate ~29 min· topic 3 of 6

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 (or use).
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.

App.jsx (trap)whole filejsx
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>
  );
}
Spot the new-object trapdiagram
Rendering diagram…

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.

App.jsx (memoised)whole filejsx
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.

ThemeProvider.jsxwhole filejsx
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>
  );
}
Fix 2: split what changes from what doesn'tdiagram
Rendering diagram…

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).

AppProviders.jsxwhole filejsx
export function AppProviders({ children }) {
  return (
    <ThemeProvider>
      <UserProvider>
        <LocaleProvider>{children}</LocaleProvider>
      </UserProvider>
    </ThemeProvider>
  );
}

Try it yourself

  1. 1

    Predict before you fix

    In the trap example, change Sidebar to read useContext(ThemeContext) (and ignore the result). Predict the logs after the unrelated click. (Sidebar rendered joins the list: reading a context is enough to be re-rendered by it, even if you don't use the value.)

  2. 2

    Take memo away

    In Fix 2, remove memo(...) from Sidebar (write it as a plain function). Predict the logs for the unrelated click. (Sidebar renders again: without memo, a child renders whenever its parent does. Context and memo are separate reasons to render; Topic 16.1 lists them all.)

  3. 3

    Turn App into a provider component

    In Fix 2, move the theme state, toggle and both providers into a ThemeProvider({ children }) component, and render <ThemeProvider><Header /><Sidebar /><ToggleButton /></ThemeProvider> from App. Remove all memo calls. Predict what renders on toggle. (Only ThemeProvider and Header: the children were created by App, which didn't render.)

Code & diagrams

Three everyday contexts: theme, current user and locale New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

en-IN ₹12,34,567.00
en-US $1,234,567.00
The trap: a new object value re-renders every reader New tab

The 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

Press Run to see the page.

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 rendered
Fix 1: memoise the value New tab

The 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

Press Run to see the page.

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 rendered
Fix 2: split the context so the button ignores theme changes New tab

Now 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

Press Run to see the page.

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

Break 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.

terminal
$ Preview
── what you'll see ──
Header (light) ← never changes, however often you click

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.

terminal
$ React DevTools Profiler
── what you'll see ──
Dozens of commits per second; every component that reads MouseContext renders on each one, and typing in inputs starts to lag.

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.is and, 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-party use-context-selector package 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 use useSyncExternalStore with 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. 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.NumberFormat and Intl.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. 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 using Object.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 reads user.name re-renders when user.avatar changes too. And context goes through memo: memo is 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. 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.is sees 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. 4

    Fix 1: memoise the value. useMemo(() => ({ theme, toggle }), [theme, toggle]) returns the same object until theme or toggle changes, and useCallback(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: the theme string in one, the toggle function (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 and dispatch, is the heart of Topic 9.6.

  5. 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, only ThemeProvider and the context's readers render; the children elements were created by its parent, so the rest of the app is skipped (Topic 9.1). Several providers are often grouped into one AppProviders component at the top of the app.

  6. 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

01

Which values are good candidates for context, and which aren't? Give examples of each.

02

Why does <Ctx value={{ user, logout }}> cause extra re-renders, and how do you fix it?

03

What does splitting a context mean and when is it worth it?

04

Does memo stop a component from re-rendering when a context it reads changes? Explain.

05

Why do state libraries with selectors render fewer components than context for the same state?

Practice

01

This provider re-renders every reader whenever App renders: <CartContext value={{ items, add: (i) => setItems([...items, i]) }}>. Fix it with useCallback and useMemo.

02

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 useMemo and useCallback.

  • 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.