Module 2: Context For Shared Client State

What goes in Context (and what doesn't)

Overview

You already know how to provide a Context (lesson 3). This lesson answers the question that decides whether your app stays clean or gets slow: what you put there. The answer isn't "everything global". Context has a hard criterion, of two conditions that hold together: what changes little AND is read by many components goes. That leaves inside three classics —the theme (light/dark), the authenticated user, the locale (language)—, and leaves out three things people put there by mistake: server state (the products, which are a cached copy of the backend → React Query, modules 4-6), what changes often (the cart, which grows with each click → a store with selectors, module 3) and what a single component uses (which stays local). You're going to see it executed as a classifier: we run each piece of Mercado's state through the criterion and out comes YES or NO, with the reason. With that, deciding what goes in Context stops being intuition and becomes a rule you can apply without hesitation.

Connection with the module. It's the module's criterion. Lesson 2 showed the problem, lesson 3 the tool; this one sets the usage rule so as not to abuse the tool. It leans on module 1's taxonomy (the four boxes) and prepares lesson 5, which proves —by measuring the re-render— why the condition "changes little" isn't a whim but a consequence of Context's cost. Here you set the criterion; lesson 5 shows you why breaking it hurts.

An analogy: the central system vs the personal fan

Go back to the house. There are things it makes sense to centralize —put them in a system the whole house shares— and things it's absurd to centralize —that are better in your own room—. The criterion for deciding which is which is exactly Context's.

The central air conditioning is centralized because it meets two conditions: all the rooms use it (many readers) and its setting changes little (you put the temperature at 22° and there it stays all day). Installing ducts throughout the house for something like that is worth it: a single source, many benefited, few changes. Same for the theme, the user, the language: half the tree reads them and they change once in a long while. Centralizing them in Context is worth it.

Now, would you centralize the fan on your desk? No. Only you use it (one reader), so there's nothing to share: it goes in your room, local. That's the state a single component uses —it doesn't go up to Context, it stays in its useState—. And would you centralize a device that changes every second, like a party strobe that flashes 10 times per second? No either: if you connect it to the central system, the whole house would flash nonstop, dizzying whoever doesn't care. That's the state that changes often —the cart—: putting it in Context would re-render the whole subtree on each click (you measure it in lesson 5), so it goes in a store with selectors, where each room decides whether it cares about the flashing. And there's one thing that isn't even the house's: the weather outside. You can show it on a screen, but your house doesn't "produce" it; it comes from an external service, it goes old, you have to query it again. That's server state —the products—: it isn't yours, it's a copy of something that lives outside (the backend), and it has its own tool (React Query). Context is for the central systems of the house that change little and everyone uses: not your fan, nor the strobe, nor the weather outside.

Worked example: the state classifier

We're going to turn the criterion into code that decides. The rule has three filters that rule out, and one condition that approves. A piece of state does NOT go in Context if: (1) it's server state (a copy of the backend), or (2) it changes often, or (3) it's used by a single component. If it survives the three filters —changes little and is read by many—, it DOES go in Context. Let's run each piece of Mercado's state through that classifier:

// What goes in Context? Criterion: (changes LITTLE) AND (read by MANY components).
// SERVER state and what changes OFTEN don't go in Context.

const candidates = [
  { name: 'theme (light/dark)',  changesOften: false, manyReaders: true,  serverState: false },
  { name: 'authenticated user',  changesOften: false, manyReaders: true,  serverState: false },
  { name: 'locale (language)',   changesOften: false, manyReaders: true,  serverState: false },
  { name: 'cart',                changesOften: true,  manyReaders: true,  serverState: false },
  { name: 'products (from API)', changesOften: false, manyReaders: true,  serverState: true  },
  { name: 'local "search" input',changesOften: true,  manyReaders: false, serverState: false },
];

function belongsInContext(c) {
  if (c.serverState)   return { ok: false, why: 'server state -> React Query (M4-M6)' };
  if (c.changesOften)  return { ok: false, why: 'changes often -> store + selectors (M3)' };
  if (!c.manyReaders)  return { ok: false, why: 'used by a single component -> local state' };
  return { ok: true, why: 'changes little and read by many -> Context' };
}

console.log('=== Which piece of state goes in Context? ===\n');
candidates.forEach((c) => {
  const v = belongsInContext(c);
  console.log(`  ${v.ok ? 'YES' : 'NO'}  ${c.name.padEnd(22)} ${v.why}`);
});

What to expect. When you run the file with Node, the output is exactly this:

=== Which piece of state goes in Context? ===

  YES  theme (light/dark)     changes little and read by many -> Context
  YES  authenticated user     changes little and read by many -> Context
  YES  locale (language)      changes little and read by many -> Context
  NO  cart                   changes often -> store + selectors (M3)
  NO  products (from API)     server state -> React Query (M4-M6)
  NO  local "search" input    changes often -> store + selectors (M3)

Read the six decisions, because they're the criterion in action. The three YES —theme, user, locale— pass the three filters: they aren't server's, they don't change often, and many read them. They're the house's central systems: centralizing them in Context is worth it. Notice they share a profile: relatively small and stable values that half the tree needs to read. That profile is the signature of "this goes in Context".

The three NO teach why each filter exists. The cart is read by many (the CartBadge, the Cart, the checkout), but it changes often —it grows with each "Add to cart"—, and that filter takes it out: it's the strobe, in Context it would flash the whole app. Its place is a store with selectors (module 3), where each component subscribes only to the part it cares about. The products are also read by many and don't change often, but they're server state —a cached copy of backend data (module 1)—, and that filter takes them out before anything else: they aren't client state, and their tool is React Query (modules 4-6), with cache and revalidation. And the local "search" input is used by a single component while being typed: there's nothing to share, it stays local (or, if the filter should be shareable by URL, it goes to searchParams, module 7). Note a nuance of the classifier: the input marks changesOften: true and manyReaders: false; the code evaluates "changes often" before "a single reader", so it reports the store's reason —but the practical conclusion is that, being used by a single component, it stays local—. What matters is the verdict: out of Context.

The moral is a rule of two conditions that hold together: changes little AND read by many. If either fails —changes often, or a single one uses it, or it isn't even yours (server)—, it doesn't go in Context. Keep the mental classifier: before putting something in a Provider, ask yourself "is it server's? does it change often? does a single one use it?". Three "no"s in a row and it's a Context candidate; a single "yes" and its tool is another.

Deep dive: the profile of a good Context value and Mercado's table

Why "changes little" is non-negotiable. It's the condition that's broken most, and lesson 5 measures it, but I'll preview the why: when a Provider's value changes, the whole subtree that consumes that Context re-renders. If the value changes once in a long while (the user turns on dark mode), that massive re-render happens rarely and isn't felt. If it changes ten times per second (the cart, an input, the scroll), that massive re-render happens ten times per second, and the app drags. "Changes little" isn't an aesthetic preference: it's what makes Context's cost acceptable.

Why server state deserves its own filter. You might think the products "change little" (the store doesn't restock every second) and "many read them", so they'd pass the criterion. But module 1 was clear: server state isn't yours, it's a cached copy of data that live in the backend, and it has properties Context doesn't dream of handling —it goes stale (old), you have to revalidate it, it's shared among users, the request can fail—. Putting it in Context forces you to reinvent by hand cache, revalidation and error handling with useEffect (exactly what react-fundamentals left as an open problem). That's why it's a separate and prioritized filter: if it's server's, don't even ask the rest; it goes to React Query.

The user's borderline case. The "authenticated user" has a part that's client state (are they logged in? their name for the greeting?) and a part that in large apps is server state (the full profile, which lives in the backend and revalidates). In this module we treat the user as a simple client value ({ name, loggedIn }) that goes in Context —it's the classic and didactic case—. In a real app, the session (the token, whether logged in) goes in Context, and the detailed profile usually comes from React Query. You'll see the fine boundary in the server modules; here, the basic user is a good Context example.

Mercado's classification table. This is the complete picture of Mercado's state spread across its boxes —the same one you'll build in the mini-project (lesson 8)—:

State piece            Box (module 1)           Changes   Tool
─────────────────────  ───────────────────────  ────────  ────────────────────────
theme (light/dark)     global client            little    Context (this module)
authenticated user     global client            little    Context (this module)
locale (language)      global client            little    Context (this module)
cart                   global client            often     store + selectors (M3)
products               server (cache)            -        React Query (M4-M6)
search / filters       URL                       -        searchParams (M7)
open local input       local                     -        useState (react-fundamentals)

Read it top to bottom: only the first three rows —global client that changes little— are Context's. The cart is global client but changes often (M3). The products aren't client's (M4-M6). The search is the URL's (M7). And an open menu or a half-typed input is local (react-fundamentals). Each box, its tool. Context occupies a precise band: global client, changes little, read by many.

Common mistakes

Putting server state in Context. What happens: you put the products (or any backend data) in a Context "because many components use them", and you end up writing by hand the cache, the refetch and the error handling with useEffect. Why it happens: the data seems global and stable. How to detect it: your Context stores data that comes from a fetch, and you have useEffect syncing it. How to fix it: server state goes in React Query (modules 4-6), which gives you cache, revalidation, dedup and errors for free. Context is for client state; backend data is another box.

Putting in Context something that changes often. What happens: you put the cart, an input's text, or the scroll position in Context, and each change re-renders the whole consuming subtree. The app feels heavy. Why it happens: "it's shared, it goes in Context". How to detect it: you interact (type, add to the cart) and half the tree re-renders (you'll see it measured in lesson 5). How to fix it: what changes often goes in a store with selectors (module 3), where each component subscribes only to the part it uses. Context is for what changes little.

Promoting to Context something a single component uses. What happens: you put in a Provider the "is this menu open?" or "which active tab?" state that only one component reads, "just in case" someone else needs it. Why it happens: after learning Context, it feels "tidier" to have everything above. How to detect it: a Context whose only consumer is one component. How to fix it: state that a single one uses stays local in its useState. Promote it to Context only when many truly need it. It's the mirror error of lifting state too much, from react-fundamentals.

Exercises

Exercise 1 — Run the classifier by hand. For each piece, apply the three filters (server? changes often? a single reader?) and give the verdict (Context / React Query / store / local): (a) the app's language; (b) the user's order list, which comes from the backend; (c) the "profile menu open/closed" state; (d) the light/dark theme.

See solution
  • (a) Language → not server, doesn't change often, read by many → Context.
  • (b) Order listis server (comes from the backend, it's a cached copy) → React Query (M4-M6). The first filter takes it out; don't even ask the rest.
  • (c) Open profile menu → not server, changes on open/close, and used by a single componentlocal (useState in that component). There's nothing to share.
  • (d) Theme → not server, doesn't change often, read by many → Context.

Only (a) and (d) —global client, changes little, many readers— are Context's.

Exercise 2 — Why doesn't the cart go in Context if "it's global"? A colleague insists: "The cart is global client state, several components use it; by your own criterion, it goes in Context". Where does their reasoning fail?

See solution

It fails in that the criterion is two conditions together, not one. The cart meets "read by many" (the CartBadge, the Cart, the checkout), but the other fails: it changes often —it grows with each "Add to cart", shrinks with each "Remove"—. And in Context, each change of the value re-renders the whole consuming subtree (lesson 5): with data that changes constantly, that's a constant massive re-render. With the analogy: the cart is the strobe, not the air conditioning; connecting it to the central system would flash the whole house. Its right tool is a store with selectors (module 3), where the CartBadge subscribes only to the count and the Cart only to the items, without re-rendering whoever doesn't care. "It's global" isn't enough; it has to be global that changes little.

Exercise 3 — Design the boxes of a chat app. Classify the state of a messaging app and say each one's tool: (a) the light/dark theme; (b) the conversation list (comes from the server); (c) the text you type in the message input; (d) the logged-in user; (e) the message that arrives in real time and updates the conversation.

See solution
  • (a) Theme → global client, changes little, many readers → Context.
  • (b) Conversation listserver (copy of the backend) → React Query (M4-M6).
  • (c) Input textlocal, used by a single component while you type → useState.
  • (d) Logged-in user → global client, changes little, many readers → Context.
  • (e) Real-time messageserver (backend data that arrives and updates the cache) → React Query (with its update mechanism); it isn't Context nor local.

Note the pattern: the cross-cutting and stable (theme, user) → Context; the backend's (conversations, messages) → React Query; the single-component's (input) → local. Classify first, choose after: module 1's lesson.

Summary and next step

In this lesson you fixed Context's criterion, which is two conditions that hold together: what changes little AND is read by many components goes —the theme, the user, the locale—. And left out are three things put there by mistake: server state (the products → React Query, because it isn't yours but a copy of the backend), what changes often (the cart → store with selectors, because in Context it would re-render everything) and what a single component uses (it stays local). You anchored it with the central system vs the personal fan: you centralize what the whole house shares and changes little, not your fan nor the strobe nor the weather outside. And you executed it as a classifier that gave three YES and three NO with their reason.

Before moving on you should be able to: apply the three filters (server? changes often? a single reader?) to any piece of state; explain why the cart, being global, doesn't go in Context; distinguish the basic user (Context) from the detailed profile (server); and place each Mercado piece in its box and its tool.

Lesson 5 proves the most important condition of this criterion: why "changes little" is non-negotiable. You're going to measure Context's re-render cost —a fat Context with { theme, user, cart } re-renders 6 consumers when the cart changes; splitting it into three drops it to 2— and you'll see the second cost, that of creating a new object value on each render which triggers spurious re-renders, and its cure (memoizing). There you'll understand, with numbers, why the strobe doesn't go in the central system.

Resources