Módulo 8: Project Manage Mercados State
La tabla de clasificación, completa
Descripción
Toda la guía empezó con una tabla y termina con una tabla. En el módulo 1 clasificaste diez piezas de Mercado en sus cuatro cajas; esa tabla fue el plano que los módulos 2 a 7 implementaron caja por caja. Esta lección la vuelve a levantar, ahora en su forma final y completa —cada pieza del storefront, su caja, su herramienta concreta y su módulo—, porque es el primer entregable del capstone y el artefacto que ordena todo lo demás. Antes de conectar una sola herramienta, produces el mapa que dice qué va dónde y con qué; las lecciones 3 a 7 no harán más que recorrer sus filas, una por una, y volverlas código.
La novedad frente al módulo 1 es una columna que allá quedó pendiente: la caja global tiene dos herramientas —Context (M2) y el store (M3)—, y esta tabla resuelve, pieza por pieza, cuál. La regla del módulo 1 clasificaba en cuatro cajas; ahora, para las piezas que caen en global, aplicamos una segunda decisión: si el dato cambia poco (el tema, el usuario), va en Context; si cambia seguido (el carrito), va en un store con selectores. La tabla completa, entonces, no clasifica en cuatro columnas sino en cinco herramientas —useState, Context, store, React Query, searchParams—, que son exactamente las que el capstone conecta.
Conexión con el módulo. Esta es la capa cero del capstone: el plano. La lección 1 mostró el método completo en miniatura; esta lo despliega en la tabla que las cinco lecciones siguientes implementan —el tema en Context (L3), el carrito en el store (L4), los productos con React Query (L5), la mutación (L6) y los filtros en la URL (L7)—. Cada una toma una o más filas de esta tabla y las conecta en el App real. Lo que produces aquí es el mapa; lo que sigue es construir la casa sobre él.
Una analogía: el plano del arquitecto antes de la obra
Nadie construye una casa empezando por poner ladrillos. Primero hay un plano: aquí la cocina, aquí las recámaras, aquí el baño, y —el detalle que importa— con qué se construye cada cosa. La cocina lleva instalación de gas y agua; las recámaras, closets; el baño, plomería especial. El plano no levanta ni una pared, pero decide todo lo que se levantará después: el electricista sabe dónde van los contactos, el plomero dónde van los tubos, el albañil dónde van los muros. Sin plano, cada oficio improvisa y la casa sale torcida —un contacto en medio de la ducha, una tubería que no llega—.
La tabla de clasificación es el plano de tu app. No conecta ninguna herramienta, pero decide cuál va dónde: el tema a Context, el carrito al store, los productos a React Query, la búsqueda a la URL. Cuando la tabla está bien hecha, cada "oficio" —cada lección siguiente— sabe exactamente qué construir y con qué, sin improvisar. Y cuando aparezca una pieza nueva (un filtro, un dato del backend), no la metes donde caiga: la pasas por la misma regla y la tabla te dice su lugar. El plano es lo que hace que la obra sea ordenada en vez de un parche sobre otro.
El código React real: la tabla, como referencia del App
La tabla no es código, es una decisión; pero apunta a un App concreto. Así se verá el storefront de Mercado con cada fila de la tabla en su herramienta —el destino que las lecciones 3 a 7 construyen—:
// Vista previa: el App de Mercado, cada caja en su herramienta (L3-L7).
function App() {
return (
<QueryClientProvider client={queryClient}> {/* SERVER: products (L5-L6) */}
<ThemeProvider> {/* GLOBAL estable: theme (L3) */}
<UserProvider> {/* GLOBAL estable: user (L3) */}
<Header /> {/* CartBadge lee el store (L4); Header lee theme/user */}
<SearchPage /> {/* URL: query/category/sort/page via searchParams (L7) */}
<CartPanel /> {/* GLOBAL seguido: cart en el store (L4) */}
</UserProvider>
</ThemeProvider>
</QueryClientProvider>
);
}
// El cart store vive FUERA del arbol (no necesita Provider).
// Cada ProductCard maneja su menuOpen con useState (LOCAL).
Fíjate en la correspondencia exacta con la tabla: el QueryClientProvider envuelve todo (los productos, server); los dos Provider de Context llevan el tema y el usuario (global estable); el SearchPage lee la URL (los filtros); el carrito vive en un store fuera del árbol (global que cambia seguido), por eso no hay <CartProvider>; y el menuOpen de cada tarjeta es useState (local). Cinco herramientas, una por fila de la tabla. Esta lección produce la tabla; el App es su consecuencia.
Ejemplo trabajado: la tabla completa, ejecutada
Corremos la clasificación sobre el inventario completo de Mercado. La función classify da la caja (M1); una segunda función, toolFor, resuelve la herramienta —incluyendo la segunda decisión dentro de global (Context vs store, según changesOften)—; y moduleFor mapea cada herramienta a su lección:
// La regla de decision (M1), extendida para elegir la herramienta final.
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: la SEGUNDA decision -> Context si cambia poco, store si cambia seguido.
return piece.changesOften ? 'store (Zustand)' : 'Context';
}
const moduleFor = {
useState: 'M1 / react-fundamentals',
Context: 'M2 (leccion 3)',
'store (Zustand)': 'M3 (leccion 4)',
'React Query': 'M4-M6 (lecciones 5-6)',
searchParams: 'M7 (leccion 7)',
};
// El inventario COMPLETO del storefront de Mercado.
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('=== Tabla de clasificacion del estado de Mercado (completo) ===\n');
console.log(pad('pieza', 18) + '| ' + pad('caja', 8) + '| ' + pad('herramienta', 16) + '| modulo');
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('\nresumen por caja: local=' + counts.local + ' global=' + counts.global +
' server=' + counts.server + ' url=' + counts.url);
console.log('resumen por herramienta:');
for (const tool of ['useState', 'Context', 'store (Zustand)', 'React Query', 'searchParams']) {
console.log(' ' + pad(tool, 16) + '-> ' + (toolCounts[tool] || 0));
}
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== Tabla de clasificacion del estado de Mercado (completo) ===
pieza | caja | herramienta | modulo
------------------+---------+-----------------+------------------------
menuOpen | local | useState | M1 / react-fundamentals
searchInputDraft | local | useState | M1 / react-fundamentals
isCartDrawerOpen | local | useState | M1 / react-fundamentals
theme | global | Context | M2 (leccion 3)
user | global | Context | M2 (leccion 3)
cart | global | store (Zustand) | M3 (leccion 4)
products | server | React Query | M4-M6 (lecciones 5-6)
reviews | server | React Query | M4-M6 (lecciones 5-6)
query | url | searchParams | M7 (leccion 7)
category | url | searchParams | M7 (leccion 7)
sort | url | searchParams | M7 (leccion 7)
page | url | searchParams | M7 (leccion 7)
resumen por caja: local=3 global=3 server=2 url=4
resumen por herramienta:
useState -> 3
Context -> 2
store (Zustand) -> 1
React Query -> 2
searchParams -> 4
Esta tabla es el plano completo de Mercado. Léela por sus tres bloques.
Las cuatro cajas, con sus piezas. Tres local (menuOpen, searchInputDraft, isCartDrawerOpen —lo privado de un componente, que muere con él—), tres global (theme, user, cart —verdad del cliente, la usan componentes lejanos—), dos server (products, reviews —cachés de datos del backend—) y cuatro url (query, category, sort, page —lo compartible—). Doce piezas, repartidas por la regla del módulo 1 sobre propiedades objetivas, no por opinión.
La segunda decisión, dentro de global. Aquí está la novedad. Las tres piezas globales no comparten herramienta: theme y user van en Context (cambian poco), y cart va en store (cambia seguido). La caja es la misma —global de cliente—, pero la herramienta se bifurca según changesOften. Por eso el resumen por caja dice global=3 pero el resumen por herramienta dice Context -> 2 y store -> 1: la caja global se partió en dos herramientas. Esta es la decisión que el módulo 1 dejó pendiente ("global → Context o store, se decide después") y que la tabla completa por fin resuelve, pieza por pieza.
Cinco herramientas, cinco lecciones. El resumen por herramienta es literalmente la agenda del capstone: useState (ya lo sabes de react-fundamentals), Context para theme/user (L3), store para cart (L4), React Query para products/reviews (L5-L6) y searchParams para los cuatro filtros (L7). Cada número de esa lista es una capa que conectamos. La tabla no solo clasifica: agenda el resto del módulo.
Profundización: por qué la tabla es el entregable, no un paso previo
Es tentador ver la tabla como un trámite —"ya sé que el carrito va en un store, ¿para qué escribirla?"— y saltar al código. Pero la tabla es un entregable del capstone, y por una razón concreta: es lo que revisas en un code review y lo que le explicas a un compañero que llega al proyecto. "El estado de Mercado está en cinco herramientas: aquí la tabla, aquí por qué cada pieza cayó donde cayó." Sin la tabla, el manejo de estado de una app es implícito —vive en la cabeza de quien lo escribió— y se degrada: el siguiente ingeniero mete un dato del servidor en el store porque no hay un plano que diga que no.
La tabla también hace explícitas las decisiones que cuestan trabajo, y en Mercado hay dos que vale la pena subrayar:
searchInputDraft(local) vs.query(url). Casi el mismo dato —texto de búsqueda—, en cajas distintas. El borrador que se teclea es privado del input (local); la búsqueda aplicada es compartible (url). La tabla los tiene en filas separadas porque son dos momentos del mismo dato, y confundirlos es el bug clásico (el historial lleno de una entrada por tecla, o los links de búsqueda que no se comparten).products/reviews(server) aunque los use media app. Caen en server por la primera pregunta —su verdad vive en el backend— y la regla corta ahí, sin importar cuántos componentes los usen. Ponerlos en el store "porque los usa toda la app" es el error que reabre el bug de copias que divergen.
Y una última: la tabla escala a lo desconocido. No es una lista cerrada de doce piezas; es una regla aplicada a doce piezas. Cuando Mercado agregue un dato mañana —una lista de deseos, un filtro nuevo, un modal—, no discutes dónde va: lo pasas por classify y toolFor, y la tabla crece con una fila más, consistente con las otras. Esa es la diferencia entre un plano y una lista: el plano tiene la regla que genera cada casilla.
Errores comunes
Tratar la tabla como opcional y saltar al código. Qué pasa: se arranca a conectar herramientas sin escribir la tabla, "porque ya se sabe dónde va cada cosa". Por qué pasa: con la práctica, clasificar se siente automático. Cómo detectarlo: llega un compañero nuevo y nadie puede decirle por qué el carrito está en el store y los productos en React Query sin improvisar. Cómo corregirlo: la tabla es el entregable que hace explícita y revisable la arquitectura de estado. Escríbela; es el documento que sobrevive a quien la escribió.
Olvidar la segunda decisión dentro de global. Qué pasa: se clasifica todo lo global —tema, usuario y carrito— en la misma herramienta (todo en Context, o todo en un store). Por qué pasa: "los tres son global, misma caja, misma herramienta". Cómo detectarlo: el carrito en Context re-renderiza medio storefront con cada clic; o el tema en un store con selectores es sobre-ingeniería para algo que cambia dos veces por sesión. Cómo corregirlo: dentro de global, aplica changesOften —cambia poco → Context, cambia seguido → store—. La caja es una; las herramientas, dos.
Meter datos del servidor en la fila global "porque los usa toda la app". Qué pasa: products termina en la fila del store o de Context, junto al carrito y el tema. Por qué pasa: se clasifica por "¿cuántos lo usan?" en vez de "¿de quién es la verdad?". Cómo detectarlo: en la tabla, un dato del backend aparece fuera de la fila server; en la app, reaparece el bug de copias stale. Cómo corregirlo: la primera pregunta —"¿la verdad es del backend?"— manda products a server antes de que "lo usan varios" importe. Si un dato remoto no está en server, la tabla se saltó el orden.
Ejercicios
Ejercicio 1 — Completa la segunda decisión. De las tres piezas globales de la tabla, theme y user fueron a Context y cart al store. Explica, con la propiedad changesOften, por qué cada una cayó donde cayó, y qué problema concreto verías si invirtieras las dos herramientas (el carrito en Context, el tema en un store).
Ver solución
Las tres son global de cliente (misma caja), pero la herramienta la decide changesOften:
themeyuser→ Context (changesOften: false): el tema se cambia una o dos veces por sesión, el usuario inicia sesión una vez. Como cambian poco, el costo de re-render de Context (todo el subárbol consumidor re-renderiza en cada cambio) es aceptable —casi nunca ocurre—.cart→ store (changesOften: true): el carrito cambia con cada "Add to cart", "Quitar", cambio de cantidad. Como cambia seguido, necesita selectores para que cada componente re-renderice solo cuando su rebanada cambia; Context haría re-renderizar a todo el subárbol en cada clic.
Si invirtieras las herramientas:
- Carrito en Context: cada operación del carrito re-renderiza a todos los consumidores del Context —el
CartBadge, elCartTotal, elCart, y cualquiera más abajo—, aunque su dato no haya cambiado. Con un carrito activo, es un desperdicio constante de re-renders (el problema que midió el módulo 3). - Tema en un store con selectores: funcionaría, pero es sobre-ingeniería: montar un store con selectores para un dato que cambia dos veces por sesión no aporta nada sobre Context, y agrega complejidad. Context es la herramienta natural de lo estable.
La regla: la caja (global) se decide por "¿la usan varios lejanos?"; la herramienta (Context vs store), por "¿cambia seguido?".
Ejercicio 2 — Justifica una fila dudosa. Un compañero propone dos cambios a la tabla: mover reviews de server a local ("solo la página de detalle las muestra, nadie más las necesita") y mover category de url a global ("es un filtro, y el store maneja filtros bien"). Evalúa ambas con la regla y di qué error comete cada uno.
Ver solución
-
reviewsa local: error. El compañero clasificó por "¿cuántos componentes la usan?" (una sola página) en vez de por "¿de quién es la verdad?". Las reseñas viven en el backend —responden "sí" a la primera pregunta—, así que son server, sin importar que solo la página de detalle las muestre. Ponerlas en local (useState) las trataría como copia propia: quedarían stale cuando alguien agregue una reseña, sin revalidación ni caché. Que un solo componente use un dato del servidor no lo saca de la caja server; lo mantiene ahí, quizá con unaqueryKeymás específica (['reviews', productId]). -
categorya global: error. El compañero clasificó por "es un filtro y el store maneja filtros" —eligió la herramienta antes de clasificar—. Perocategoryresponde "sí" a la segunda pregunta ("¿compartible / sobrevive al reload?"): es un filtro que el usuario querría compartir por link y recuperar tras recargar. Va en url. En el store,categoryno sería compartible, no sobreviviría al reload, y el botón "atrás" no la desharía —perderías las tres virtudes de la URL—. Que el store "maneje filtros" no significa que deba: la URL los maneja mejor porque les da compartir, bookmark y back/forward gratis.
Los dos errores son el mismo de fondo: clasificar por algo que no es la pregunta correcta (cuántos lo usan, qué herramienta suena bien) en vez de por la regla en orden.
Ejercicio 3 — Amplía la tabla. Mercado agrega tres piezas nuevas: wishlist (la lista de deseos guardada en tu cuenta), notificationsPanelOpen (si el panel de notificaciones está desplegado) y locale (el idioma elegido, que cambia poco y lo lee casi todo texto). Clasifica cada una con classify y toolFor, di su herramienta, y muestra cómo quedarían los dos resúmenes actualizados.
Ver solución
wishlist→ server → React Query. Su verdad vive en el backend (truthLivesInBackend: true). Primera pregunta: sí. Es un caché, comoproductsyreviews.queryKeypropia:['wishlist'].notificationsPanelOpen→ local → useState. No es del backend, no se comparte por link, lo usa el componente del panel; muere al cerrarse. Cae hasta local.locale→ global → Context. Es global de cliente (neededByDistantComponents: true) y cambia poco (changesOften: false), así que la segunda decisión lo manda a Context, junto athemeyuser.
Resúmenes actualizados (las doce originales + estas tres):
- Por caja: local=4 (
menuOpen,searchInputDraft,isCartDrawerOpen,notificationsPanelOpen), global=4 (theme,user,cart,locale), server=3 (products,reviews,wishlist), url=4 (query,category,sort,page). - Por herramienta: useState=4, Context=3 (
theme,user,locale), store=1 (cart), React Query=3, searchParams=4.
Las tres nuevas siguieron la misma regla, sin excepción —y locale mostró de nuevo la segunda decisión: global, pero a Context por cambiar poco—.
Resumen y siguiente paso
En esta lección produjiste el plano completo del estado de Mercado: la tabla que clasifica las doce piezas del storefront en sus cuatro cajas y —la novedad frente al módulo 1— resuelve la segunda decisión dentro de global, repartiendo theme/user a Context y cart al store según changesOften. La corrida dio el resumen por caja (local=3, global=3, server=2, url=4) y, sobre todo, el resumen por herramienta (useState=3, Context=2, store=1, React Query=2, searchParams=4), que es literalmente la agenda del resto del módulo. Y viste por qué la tabla es un entregable, no un trámite: es el documento revisable que hace explícita la arquitectura de estado, hace visibles las decisiones que cuestan (el borrador vs. la búsqueda, los datos del servidor que usan muchos), y escala a piezas futuras porque tiene la regla, no solo la lista.
Antes de avanzar deberías poder: clasificar el inventario completo de una app en sus cinco herramientas; aplicar la segunda decisión (Context vs store) dentro de global; justificar cada fila con la regla en orden; y ampliar la tabla ante una pieza nueva sin discutir.
La lección 3 conecta la primera fila del plano: el tema (y el usuario) en Context. Vas a montar el ThemeProvider con su reducer y el UserProvider, cada uno en su propio Context, y a verlos —ejecutado— fluir por sus ductos mientras el carrito, los productos y la búsqueda esperan su turno en sus cajas. La tabla dice qué; la lección 3 empieza a construir el cómo.
Recursos
- React, "Managing State" — react.dev/learn/managing-state. El recorrido oficial por las decisiones de estado; la tabla de esta lección es su aplicación completa al caso Mercado. En inglés.
- React, "Choosing the State Structure" — react.dev/learn/choosing-the-state-structure. Los criterios para estructurar el estado antes de tocar herramientas —el "clasifica primero" que la tabla materializa—. En inglés.
- TkDodo, "React Query as a State Manager" — tkdodo.eu/blog/react-query-as-a-state-manager. Por qué el estado del servidor es una fila aparte de la tabla, con su herramienta —el fundamento de que
products/reviewsvan a React Query, no al store—. En inglés. - TanStack Query — tanstack.com/query/latest. La herramienta de la caja server, destino de las filas
productsyreviews, que las lecciones 5 y 6 conectan. En inglés.