Module 8: Project Manage Mercados State
The classification table, complete
Overview
The whole guide started with a table and ends with a table. In module 1 you classified ten pieces of Mercado in their four boxes; that table was the blueprint that modules 2 to 7 implemented box by box. This lesson raises it again, now in its final and complete form —each piece of the storefront, its box, its concrete tool and its module—, because it's the first deliverable of the capstone and the artifact that orders everything else. Before connecting a single tool, you produce the map that says what goes where and with what; lessons 3 to 7 will do nothing more than walk its rows, one by one, and turn them into code.
The novelty relative to module 1 is a column that was left pending there: the global box has two tools —Context (M2) and the store (M3)—, and this table resolves, piece by piece, which one. Module 1's rule classified into four boxes; now, for the pieces that fall in global, we apply a second decision: if the data changes little (the theme, the user), it goes in Context; if it changes often (the cart), it goes in a store with selectors. The complete table, then, doesn't classify into four columns but into five tools —useState, Context, store, React Query, searchParams—, which are exactly the ones the capstone connects.
Connection with the module. This is the capstone's layer zero: the blueprint. Lesson 1 showed the complete method in miniature; this one unfolds it in the table that the five following lessons implement —the theme in Context (L3), the cart in the store (L4), the products with React Query (L5), the mutation (L6) and the filters in the URL (L7)—. Each one takes one or more rows of this table and connects them in the real App. What you produce here is the map; what follows is building the house over it.
An analogy: the architect's blueprint before the construction
Nobody builds a house starting by laying bricks. First there's a blueprint: here the kitchen, here the bedrooms, here the bathroom, and —the detail that matters— what each thing is built with. The kitchen carries gas and water installation; the bedrooms, closets; the bathroom, special plumbing. The blueprint doesn't raise a single wall, but it decides everything that will be raised afterward: the electrician knows where the outlets go, the plumber where the pipes go, the builder where the walls go. Without a blueprint, each trade improvises and the house comes out crooked —an outlet in the middle of the shower, a pipe that doesn't reach—.
The classification table is your app's blueprint. It connects no tool, but it decides which goes where: the theme to Context, the cart to the store, the products to React Query, the search to the URL. When the table is well made, each "trade" —each following lesson— knows exactly what to build and with what, without improvising. And when a new piece appears (a filter, a piece of backend data), you don't put it wherever it lands: you pass it through the same rule and the table tells you its place. The blueprint is what makes the construction orderly instead of a patch on top of another.
The real React code: the table, as a reference for the App
The table isn't code, it's a decision; but it points to a concrete App. This is how Mercado's storefront will look with each row of the table in its tool —the destination that lessons 3 to 7 build—:
// Preview: Mercado's App, each box in its tool (L3-L7).
function App() {
return (
<QueryClientProvider client={queryClient}> {/* SERVER: products (L5-L6) */}
<ThemeProvider> {/* GLOBAL stable: theme (L3) */}
<UserProvider> {/* GLOBAL stable: user (L3) */}
<Header /> {/* CartBadge reads the store (L4); Header reads theme/user */}
<SearchPage /> {/* URL: query/category/sort/page via searchParams (L7) */}
<CartPanel /> {/* GLOBAL often: cart in the store (L4) */}
</UserProvider>
</ThemeProvider>
</QueryClientProvider>
);
}
// The cart store lives OUTSIDE the tree (needs no Provider).
// Each ProductCard manages its menuOpen with useState (LOCAL).
Notice the exact correspondence with the table: the QueryClientProvider wraps everything (the products, server); the two Context Providers carry the theme and the user (global stable); the SearchPage reads the URL (the filters); the cart lives in a store outside the tree (global that changes often), that's why there's no <CartProvider>; and each card's menuOpen is useState (local). Five tools, one per row of the table. This lesson produces the table; the App is its consequence.
Worked example: the complete table, executed
We run the classification over Mercado's complete inventory. The classify function gives the box (M1); a second function, toolFor, resolves the tool —including the second decision within global (Context vs store, according to changesOften)—; and moduleFor maps each tool to its lesson:
// The decision rule (M1), extended to choose the final tool.
function classify(piece) {
if (piece.truthLivesInBackend) return 'server';
if (piece.shareableAndBookmarkable) return 'url';
if (piece.neededByDistantComponents) return 'global';
return 'local';
}
function toolFor(piece, box) {
if (box === 'local') return 'useState';
if (box === 'server') return 'React Query';
if (box === 'url') return 'searchParams';
// global: the SECOND decision -> Context if it changes little, store if it changes often.
return piece.changesOften ? 'store (Zustand)' : 'Context';
}
const moduleFor = {
useState: 'M1 / react-fundamentals',
Context: 'M2 (lesson 3)',
'store (Zustand)': 'M3 (lesson 4)',
'React Query': 'M4-M6 (lessons 5-6)',
searchParams: 'M7 (lesson 7)',
};
// The COMPLETE inventory of Mercado's storefront.
const inventory = [
{ name: 'menuOpen', truthLivesInBackend: false, shareableAndBookmarkable: false, neededByDistantComponents: false, changesOften: true },
{ name: 'searchInputDraft', truthLivesInBackend: false, shareableAndBookmarkable: false, neededByDistantComponents: false, changesOften: true },
{ name: 'isCartDrawerOpen', truthLivesInBackend: false, shareableAndBookmarkable: false, neededByDistantComponents: false, changesOften: true },
{ name: 'theme', truthLivesInBackend: false, shareableAndBookmarkable: false, neededByDistantComponents: true, changesOften: false },
{ name: 'user', truthLivesInBackend: false, shareableAndBookmarkable: false, neededByDistantComponents: true, changesOften: false },
{ name: 'cart', truthLivesInBackend: false, shareableAndBookmarkable: false, neededByDistantComponents: true, changesOften: true },
{ name: 'products', truthLivesInBackend: true, shareableAndBookmarkable: false, neededByDistantComponents: true, changesOften: false },
{ name: 'reviews', truthLivesInBackend: true, shareableAndBookmarkable: false, neededByDistantComponents: false, changesOften: false },
{ name: 'query', truthLivesInBackend: false, shareableAndBookmarkable: true, neededByDistantComponents: true, changesOften: true },
{ name: 'category', truthLivesInBackend: false, shareableAndBookmarkable: true, neededByDistantComponents: true, changesOften: false },
{ name: 'sort', truthLivesInBackend: false, shareableAndBookmarkable: true, neededByDistantComponents: true, changesOften: false },
{ name: 'page', truthLivesInBackend: false, shareableAndBookmarkable: true, neededByDistantComponents: true, changesOften: false },
];
const pad = (s, n) => (s + ' '.repeat(n)).slice(0, n);
console.log('=== Mercado state classification table (complete) ===\n');
console.log(pad('piece', 18) + '| ' + pad('box', 8) + '| ' + pad('tool', 16) + '| module');
console.log('-'.repeat(18) + '+' + '-'.repeat(9) + '+' + '-'.repeat(17) + '+' + '-'.repeat(24));
const counts = { local: 0, global: 0, server: 0, url: 0 };
const toolCounts = {};
for (const piece of inventory) {
const box = classify(piece);
const tool = toolFor(piece, box);
counts[box]++;
toolCounts[tool] = (toolCounts[tool] || 0) + 1;
console.log(pad(piece.name, 18) + '| ' + pad(box, 8) + '| ' + pad(tool, 16) + '| ' + moduleFor[tool]);
}
console.log('\nsummary by box: local=' + counts.local + ' global=' + counts.global +
' server=' + counts.server + ' url=' + counts.url);
console.log('summary by tool:');
for (const tool of ['useState', 'Context', 'store (Zustand)', 'React Query', 'searchParams']) {
console.log(' ' + pad(tool, 16) + '-> ' + (toolCounts[tool] || 0));
}
What to expect. When you run the file with Node, the output is exactly this:
=== Mercado state classification table (complete) ===
piece | box | tool | module
------------------+---------+-----------------+------------------------
menuOpen | local | useState | M1 / react-fundamentals
searchInputDraft | local | useState | M1 / react-fundamentals
isCartDrawerOpen | local | useState | M1 / react-fundamentals
theme | global | Context | M2 (lesson 3)
user | global | Context | M2 (lesson 3)
cart | global | store (Zustand) | M3 (lesson 4)
products | server | React Query | M4-M6 (lessons 5-6)
reviews | server | React Query | M4-M6 (lessons 5-6)
query | url | searchParams | M7 (lesson 7)
category | url | searchParams | M7 (lesson 7)
sort | url | searchParams | M7 (lesson 7)
page | url | searchParams | M7 (lesson 7)
summary by box: local=3 global=3 server=2 url=4
summary by tool:
useState -> 3
Context -> 2
store (Zustand) -> 1
React Query -> 2
searchParams -> 4
This table is Mercado's complete blueprint. Read it by its three blocks.
The four boxes, with their pieces. Three local (menuOpen, searchInputDraft, isCartDrawerOpen —what's private to a component, which dies with it—), three global (theme, user, cart —client truth, distant components use them—), two server (products, reviews —caches of backend data—) and four url (query, category, sort, page —the shareable—). Twelve pieces, distributed by module 1's rule over objective properties, not by opinion.
The second decision, within global. Here's the novelty. The three global pieces do not share a tool: theme and user go in Context (they change little), and cart goes in store (it changes often). The box is the same —global client—, but the tool forks according to changesOften. That's why the summary by box says global=3 but the summary by tool says Context -> 2 and store -> 1: the global box was split into two tools. This is the decision module 1 left pending ("global → Context or store, decided later") and that the complete table finally resolves, piece by piece.
Five tools, five lessons. The summary by tool is literally the capstone's agenda: useState (you already know it from react-fundamentals), Context for theme/user (L3), store for cart (L4), React Query for products/reviews (L5-L6) and searchParams for the four filters (L7). Each number of that list is a layer we connect. The table doesn't just classify: it schedules the rest of the module.
Deeper: why the table is the deliverable, not a preliminary step
It's tempting to see the table as a formality —"I already know the cart goes in a store, why write it?"— and jump to the code. But the table is a capstone deliverable, and for a concrete reason: it's what you review in a code review and what you explain to a colleague who joins the project. "Mercado's state is in five tools: here's the table, here's why each piece fell where it fell." Without the table, an app's state management is implicit —it lives in the head of whoever wrote it— and degrades: the next engineer puts a piece of server data in the store because there's no blueprint saying not to.
The table also makes explicit the decisions that take work, and in Mercado there are two worth underlining:
searchInputDraft(local) vs.query(url). Almost the same data —search text—, in different boxes. The draft being typed is private to the input (local); the applied search is shareable (url). The table has them in separate rows because they're two moments of the same data, and confusing them is the classic bug (the history full of one entry per keystroke, or search links that aren't shared).products/reviews(server) even if half the app uses them. They fall in server because of the first question —their truth lives in the backend— and the rule cuts there, regardless of how many components use them. Putting them in the store "because the whole app uses them" is the mistake that reopens the diverging-copies bug.
And a last one: the table scales to the unknown. It's not a closed list of twelve pieces; it's a rule applied to twelve pieces. When Mercado adds a piece of data tomorrow —a wishlist, a new filter, a modal—, you don't argue about where it goes: you pass it through classify and toolFor, and the table grows with one more row, consistent with the others. That's the difference between a blueprint and a list: the blueprint has the rule that generates each cell.
Common mistakes
Treating the table as optional and jumping to the code. What happens: it starts connecting tools without writing the table, "because everyone knows where each thing goes". Why it happens: with practice, classifying feels automatic. How to detect it: a new colleague arrives and nobody can tell them why the cart is in the store and the products in React Query without improvising. How to fix it: the table is the deliverable that makes the state architecture explicit and reviewable. Write it; it's the document that survives whoever wrote it.
Forgetting the second decision within global. What happens: everything global —theme, user and cart— is classified in the same tool (all in Context, or all in a store). Why it happens: "the three are global, same box, same tool". How to detect it: the cart in Context re-renders half the storefront on each click; or the theme in a store with selectors is over-engineering for something that changes twice per session. How to fix it: within global, apply changesOften —changes little → Context, changes often → store—. The box is one; the tools, two.
Putting server data in the global row "because the whole app uses it". What happens: products ends up in the store or Context row, next to the cart and the theme. Why it happens: it's classified by "how many use it?" instead of "whose truth is it?". How to detect it: in the table, a piece of backend data appears outside the server row; in the app, the stale-copies bug reappears. How to fix it: the first question —"is the truth the backend's?"— sends products to server before "several use it" matters. If a remote piece of data isn't in server, the table skipped the order.
Exercises
Exercise 1 — Complete the second decision. Of the three global pieces of the table, theme and user went to Context and cart to the store. Explain, with the changesOften property, why each fell where it fell, and what concrete problem you'd see if you inverted the two tools (the cart in Context, the theme in a store).
See solution
The three are global client (same box), but the tool is decided by changesOften:
themeanduser→ Context (changesOften: false): the theme is changed once or twice per session, the user logs in once. Since they change little, the re-render cost of Context (the whole consuming subtree re-renders on each change) is acceptable —it almost never happens—.cart→ store (changesOften: true): the cart changes with each "Add to cart", "Remove", quantity change. Since it changes often, it needs selectors so each component re-renders only when its slice changes; Context would re-render the whole subtree on each click.
If you inverted the tools:
- Cart in Context: each cart operation re-renders all the Context consumers —the
CartBadge, theCartTotal, theCart, and anyone further down—, even if their data didn't change. With an active cart, it's a constant waste of re-renders (the problem module 3 measured). - Theme in a store with selectors: it would work, but it's over-engineering: setting up a store with selectors for a piece of data that changes twice per session adds nothing over Context, and adds complexity. Context is the natural tool of the stable.
The rule: the box (global) is decided by "do several distant ones use it?"; the tool (Context vs store), by "does it change often?".
Exercise 2 — Justify a doubtful row. A colleague proposes two changes to the table: move reviews from server to local ("only the detail page shows them, nobody else needs them") and move category from url to global ("it's a filter, and the store handles filters well"). Evaluate both with the rule and say what mistake each makes.
See solution
-
reviewsto local: mistake. The colleague classified by "how many components use it?" (a single page) instead of by "whose truth is it?". The reviews live in the backend —they answer "yes" to the first question—, so they're server, regardless of only the detail page showing them. Putting them in local (useState) would treat them as an own copy: they'd stay stale when someone adds a review, without revalidation nor cache. That a single component uses a piece of server data doesn't take it out of the server box; it keeps it there, perhaps with a more specificqueryKey(['reviews', productId]). -
categoryto global: mistake. The colleague classified by "it's a filter and the store handles filters" —they chose the tool before classifying—. Butcategoryanswers "yes" to the second question ("shareable / survives the reload?"): it's a filter the user would want to share by link and recover after reloading. It goes in url. In the store,categorywouldn't be shareable, wouldn't survive the reload, and the "back" button wouldn't undo it —you'd lose the three virtues of the URL—. That the store "handles filters" doesn't mean it should: the URL handles them better because it gives them share, bookmark and back/forward for free.
The two mistakes are the same at bottom: classifying by something that isn't the right question (how many use it, which tool sounds good) instead of by the rule in order.
Exercise 3 — Extend the table. Mercado adds three new pieces: wishlist (the wishlist saved in your account), notificationsPanelOpen (whether the notifications panel is expanded) and locale (the chosen language, which changes little and almost every text reads). Classify each with classify and toolFor, say its tool, and show how the two summaries would be updated.
See solution
wishlist→ server → React Query. Its truth lives in the backend (truthLivesInBackend: true). First question: yes. It's a cache, likeproductsandreviews. OwnqueryKey:['wishlist'].notificationsPanelOpen→ local → useState. It's not the backend's, it's not shared by link, the panel component uses it; it dies on closing. It falls all the way to local.locale→ global → Context. It's global client (neededByDistantComponents: true) and changes little (changesOften: false), so the second decision sends it to Context, next tothemeanduser.
Updated summaries (the twelve originals + these three):
- By box: local=4 (
menuOpen,searchInputDraft,isCartDrawerOpen,notificationsPanelOpen), global=4 (theme,user,cart,locale), server=3 (products,reviews,wishlist), url=4 (query,category,sort,page). - By tool: useState=4, Context=3 (
theme,user,locale), store=1 (cart), React Query=3, searchParams=4.
The three new ones followed the same rule, without exception —and locale showed the second decision again: global, but to Context for changing little—.
Summary and next step
In this lesson you produced Mercado's state complete blueprint: the table that classifies the twelve pieces of the storefront in their four boxes and —the novelty relative to module 1— resolves the second decision within global, distributing theme/user to Context and cart to the store according to changesOften. The run gave the summary by box (local=3, global=3, server=2, url=4) and, above all, the summary by tool (useState=3, Context=2, store=1, React Query=2, searchParams=4), which is literally the agenda of the rest of the module. And you saw why the table is a deliverable, not a formality: it's the reviewable document that makes the state architecture explicit, makes visible the decisions that take work (the draft vs. the search, the server data that many use), and scales to future pieces because it has the rule, not just the list.
Before moving on you should be able to: classify an app's complete inventory into its five tools; apply the second decision (Context vs store) within global; justify each row with the rule in order; and extend the table for a new piece without arguing.
Lesson 3 connects the first row of the blueprint: the theme (and the user) in Context. You're going to set up the ThemeProvider with its reducer and the UserProvider, each in its own Context, and see them —executed— flow through their ducts while the cart, the products and the search wait their turn in their boxes. The table says what; lesson 3 starts building the how.
Resources
- React, "Managing State" — react.dev/learn/managing-state. The official tour of state decisions; this lesson's table is its complete application to the Mercado case. In English.
- React, "Choosing the State Structure" — react.dev/learn/choosing-the-state-structure. The criteria for structuring state before touching tools —the "classify first" the table materializes—. In English.
- TkDodo, "React Query as a State Manager" — tkdodo.eu/blog/react-query-as-a-state-manager. Why server state is a separate row of the table, with its tool —the foundation that
products/reviewsgo to React Query, not the store—. In English. - TanStack Query — tanstack.com/query/latest. The tool of the server box, destination of the
productsandreviewsrows, which lessons 5 and 6 connect. In English.