Módulo 3: Global Client State With A Store
Mini-proyecto: maneja el carrito en un store
Descripción
Llegó el momento de juntar el módulo entero en una sola pieza. Vas a manejar el carrito de Mercado de punta a punta con un store, con las tres partes que ya sabes armar: la tabla de clasificación que ubica cada dato de Mercado en su herramienta, el código React real del store y sus consumidores, y una sesión ejecutada en Node con tres suscriptores —el CartBadge (conteo), el CartTotal (total) y el Cart (detalle)— donde cada uno reacciona solo a su rebanada, con el contraste medido contra Context. Este mini-proyecto es la síntesis: reúne el store fuera de React, las acciones que producen estado nuevo, los selectores finos, la decisión de que el carrito va en un store, y la frontera (los productos, del servidor, quedan fuera). Es el storefront con su estado que cambia seguido, por fin en su lugar.
Conexión con el módulo. Es el capstone. Reúne las siete lecciones: el problema del carrito que cambia seguido (L1), el store fuera de React (L2), el estado y las acciones juntos (L3), la suscripción con selector (L4), los selectores bien escritos (L5), la decisión store/Context/levantar (L6) y la aplicación a Mercado (L7). Y cierra la frontera del módulo: el estado global de cliente que cambia seguido (el carrito) se resuelve con un store; el que cambia poco (tema, usuario) sigue en Context (M2); el del servidor (productos) queda apuntado a React Query (M4-M6), y el de la URL (búsqueda) a searchParams (M7). Con esto terminas el módulo del store y quedas listo para la mitad más importante de la guía: el estado del servidor.
El plan: qué vamos a armar
El carrito tiene tres reglas de diseño que salen del módulo:
- Clasifica antes de elegir. Cada pieza de estado de Mercado va en su herramienta (módulo 1 y 6). Solo lo global de cliente que cambia seguido y lo leen/escriben muchos va en un store: el carrito.
- Estado y acciones juntos, fuera del árbol, con selectores. El
itemsy sus acciones (addItem/removeItem/clear) en un mismo store fuera de React (L2, L3); cada componente suscrito a su rebanada con un selector fino (L4, L5), para que un cambio toque solo a quien lo usa. - Lo que no es del carrito, fuera del store. El tema y el usuario no van aquí (cambian poco → Context, M2); los productos tampoco (son del servidor → React Query, M4-M6); la búsqueda tampoco (va en la URL → M7); un menú abierto tampoco (es local →
useState).
La tabla de clasificación del estado
Antes de una línea de código, la decisión hecha explícita. Esta es la foto del estado de Mercado repartido en sus herramientas, con el carrito ya en su lugar:
Pieza de estado Que es Herramienta
───────────────────── ──────────────────────────────── ─────────────────────────────
cart (items) global de cliente, cambia SEGUIDO store + selectores (este modulo)
theme / user global de cliente, cambia poco Context (M2)
products del servidor (cache) React Query (M4-M6)
search / filters de la URL searchParams (M7)
dropdown / menu abierto local a un componente useState (levantar)
Léela como el resumen de dónde estás en la guía: el carrito por fin tiene su herramienta (este módulo); el tema y el usuario ya la tienen (Context, M2); y quedan dos cajas por resolver —los productos (servidor, M4-M6) y la búsqueda (URL, M7)—. Este proyecto arma la primera fila y deja las demás señaladas a su módulo.
El árbol y el flujo de datos
Este es el storefront con el carrito en un store fuera del árbol, y tres consumidores suscritos cada uno a su rebanada. Los productos aparecen aparte, apuntando a su herramienta:
flowchart TD
Store["useCartStore (fuera del arbol: items + addItem/removeItem/clear)"]
App[App]
Header --> Badge["CartBadge (selectCount)"]
App --> Header
App --> Grid[ProductGrid]
Grid --> Card["ProductCard (s => s.addItem)"]
App --> Panel[CartPanel]
Panel --> Total["CartTotal (selectTotal)"]
Panel --> Cart["Cart (selectItems)"]
Server["products -> React Query (M4-M6, NO el store)"]
Badge -. "count" .-> Store
Card -. "addItem" .-> Store
Total -. "total" .-> Store
Cart -. "items" .-> Store
Léelo con lo aprendido. El useCartStore es la pizarra central, fuera del árbol, y los componentes se conectan con líneas punteadas —cada uno su rebanada—: el CartBadge lee el conteo (en el Header), el CartTotal el total y el Cart el detalle (en el CartPanel), y el ProductCard (en la grilla) solo usa la acción addItem. Cuatro componentes en tres ramas lejanas, todos hablando directo con el mismo store, sin prop drilling. Y los productos están fuera, con una nota: su herramienta es React Query (M4-M6), no el store.
El código React real
Este es el carrito de Mercado tal como lo escribirías con Zustand. Léelo pieza por pieza, notando dónde aterriza cada lección.
El store — estado y acciones juntos, fuera del árbol (L2, L3).
import { create } from 'zustand';
const useCartStore = create((set) => ({
items: [], // estado (la unica rebanada base)
addItem: (product) => set((s) => ({ items: [...s.items, product] })), // no muta: crea nuevo
removeItem: (id) => set((s) => ({ items: s.items.filter((it) => it.id !== id) })),
clear: () => set({ items: [] }),
}));
// Selectores reutilizables: derivan de items, devuelven primitivos/referencias estables (L5).
const selectCount = (s) => s.items.length;
const selectTotal = (s) => s.items.reduce((a, it) => a + it.priceCents, 0);
const selectItems = (s) => s.items;
El store guarda solo items (L3: guarda el dato base, deriva el resto); las acciones producen estado nuevo sin mutar (L3); y los selectores derivan lo que cada componente necesita, devolviendo primitivos donde se puede (selectCount, selectTotal) para no re-renderizar de más (L5).
Los consumidores — cada uno a su rebanada (L4).
function CartBadge() { // en el Header
const count = useCartStore(selectCount); // solo el conteo -> re-render si cambia el numero
return <span className="cart-badge">{count}</span>;
}
function CartTotal() { // en el CartPanel
const total = useCartStore(selectTotal); // solo el total -> re-render si cambia el total
return <strong>Total: {formatPrice(total)}</strong>;
}
function Cart() { // en el CartPanel
const items = useCartStore(selectItems); // la lista -> re-render si cambia la lista
const remove = useCartStore((s) => s.removeItem); // solo la accion (identidad estable)
return (
<ul>
{items.map((it) => (
<li key={it.id}>
{it.name} — {formatPrice(it.priceCents)}
<button onClick={() => remove(it.id)}>Quitar</button>
</li>
))}
</ul>
);
}
function ProductCard({ product }) { // en la grilla de productos
const addItem = useCartStore((s) => s.addItem); // solo la accion -> no re-renderiza por el carrito
return <button onClick={() => addItem(product)}>Add to cart</button>;
}
Ninguno recibe el carrito por props: los cuatro leen del store con su selector. El CartBadge el conteo, el CartTotal el total, el Cart el detalle, y el ProductCard solo la acción addItem —que, por su identidad estable, no lo hace re-renderizar cuando el carrito cambia (L3)—. El formatPrice(2599) → "$25.99" es el de siempre.
El App — no necesita Provider para el carrito.
function App() {
// El cart store vive FUERA del arbol: el App NO envuelve nada para el carrito.
// (El tema y el usuario si van en sus Providers de Context, del modulo 2.)
return (
<ThemeProvider>
<UserProvider>
<Header /> {/* contiene CartBadge */}
<ProductGrid /> {/* contiene ProductCards */}
<CartPanel /> {/* contiene CartTotal y Cart */}
</UserProvider>
</ThemeProvider>
);
}
Fíjate en lo que no hay: ningún <CartProvider>. El carrito no necesita Provider porque el store vive fuera del árbol; cualquier componente lo lee directo con useCartStore. Los únicos Providers son los del módulo 2 (tema y usuario), que sí usan Context. Store y Context conviven, cada uno para lo suyo.
Ejemplo trabajado: una sesión, ejecutada
Vamos a correr el carrito simulando una sesión de compra: primero imprimimos la tabla de clasificación, luego el usuario agrega y quita productos —incluido un sticker gratis— con tres suscriptores mirando (CartBadge al conteo, CartTotal al total, Cart al detalle), y al final comparamos los re-renders contra Context. Verás que cada suscriptor reacciona solo a su columna, y que el producto gratis (que sube el conteo pero no el total) no despierta al CartTotal:
// ---- mini store estilo Zustand + suscripcion con selector (sin dependencias) ----
function createStore(createState) {
let state;
const listeners = new Set();
const getState = () => state;
const setState = (partial) => {
const next = typeof partial === 'function' ? partial(state) : partial;
state = { ...state, ...next };
listeners.forEach((l) => l(state));
};
const subscribe = (listener) => { listeners.add(listener); return () => listeners.delete(listener); };
state = createState(setState, getState);
return { getState, setState, subscribe };
}
function subscribeWithSelector(store, selector, onChange) {
let current = selector(store.getState());
return store.subscribe((state) => {
const next = selector(state);
if (!Object.is(next, current)) { current = next; onChange(next); }
});
}
const formatPrice = (cents) => `$${(cents / 100).toFixed(2)}`;
// ---- 1) la tabla de clasificacion del estado de Mercado ----
const stateMap = [
['cart', 'global de cliente, cambia SEGUIDO', 'store + selectores (este modulo)'],
['theme / user', 'global de cliente, cambia poco', 'Context (M2)'],
['products', 'estado del SERVIDOR (cache)', 'React Query (M4-M6)'],
['search / filters', 'estado de la URL', 'searchParams (M7)'],
['dropdown abierto', 'local a un componente', 'useState (levantar)'],
];
console.log('=== Clasificacion del estado de Mercado ===\n');
console.log(' Pieza | Que es | Herramienta');
console.log(' --------------------|-------------------------------------|---------------------------------');
stateMap.forEach(([p, w, t]) => console.log(` ${p.padEnd(19)} | ${w.padEnd(35)} | ${t}`));
// ---- 2) el cart store: estado + acciones ----
const useCartStore = createStore((set) => ({
items: [],
addItem: (product) => set((s) => ({ items: [...s.items, product] })),
removeItem: (id) => set((s) => ({ items: s.items.filter((it) => it.id !== id) })),
clear: () => set({ items: [] }),
}));
const selectCount = (s) => s.items.length;
const selectTotal = (s) => s.items.reduce((sum, it) => sum + it.priceCents, 0);
const selectItems = (s) => s.items;
// ---- 3) tres suscriptores con selectores ----
let badge = 0, totalR = 0, cart = 0;
subscribeWithSelector(useCartStore, selectCount, (c) => { badge++; console.log(` CartBadge -> (${c})`); });
subscribeWithSelector(useCartStore, selectTotal, (t) => { totalR++; console.log(` CartTotal -> ${formatPrice(t)}`); });
subscribeWithSelector(useCartStore, selectItems, (items) => {
cart++;
console.log(` Cart -> ${items.map((it) => it.name).join(', ') || '(vacio)'}`);
});
const { addItem, removeItem, clear } = useCartStore.getState();
console.log('\n=== Sesion de compra (el carrito en el store, con selectores) ===\n');
console.log('addItem(Wireless Mouse, 2599) -> count, total y items cambian:');
addItem({ id: 1, name: 'Wireless Mouse', priceCents: 2599 });
console.log('\naddItem(Free Sticker, 0) -> count e items cambian; total NO:');
addItem({ id: 2, name: 'Free Sticker', priceCents: 0 });
console.log('\naddItem(Mechanical Keyboard, 8900) -> count, total e items cambian:');
addItem({ id: 3, name: 'Mechanical Keyboard', priceCents: 8900 });
console.log('\nremoveItem(2 = Free Sticker) -> count e items cambian; total NO:');
removeItem(2);
console.log('\nclear() -> los tres cambian:');
clear();
console.log(`\n Re-renders con selectores -> CartBadge: ${badge} | CartTotal: ${totalR} | Cart: ${cart}`);
// ---- 4) contraste con Context (sin selectores): todos re-renderizan siempre ----
console.log('\n=== El mismo guion en Context (sin selectores): todos re-renderizan SIEMPRE ===\n');
const ctx = createStore((set) => ({
items: [],
addItem: (p) => set((s) => ({ items: [...s.items, p] })),
removeItem: (id) => set((s) => ({ items: s.items.filter((it) => it.id !== id) })),
clear: () => set({ items: [] }),
}));
let cBadge = 0, cTotal = 0, cCart = 0;
ctx.subscribe(() => cBadge++);
ctx.subscribe(() => cTotal++);
ctx.subscribe(() => cCart++);
const ca = ctx.getState();
ca.addItem({ id: 1, name: 'Wireless Mouse', priceCents: 2599 });
ca.addItem({ id: 2, name: 'Free Sticker', priceCents: 0 });
ca.addItem({ id: 3, name: 'Mechanical Keyboard', priceCents: 8900 });
ca.removeItem(2);
ca.clear();
console.log(` Re-renders con Context -> CartBadge: ${cBadge} | CartTotal: ${cTotal} | Cart: ${cCart}`);
console.log(`\n CartTotal: ${totalR} re-renders con selector vs ${cTotal} con Context.`);
console.log(' Con selectores, el Free Sticker (total sin cambio) no toco a CartTotal.');
console.log('\n=== products NO paso por el store: es del servidor -> React Query (M4-M6) ===');
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== Clasificacion del estado de Mercado ===
Pieza | Que es | Herramienta
--------------------|-------------------------------------|---------------------------------
cart | global de cliente, cambia SEGUIDO | store + selectores (este modulo)
theme / user | global de cliente, cambia poco | Context (M2)
products | estado del SERVIDOR (cache) | React Query (M4-M6)
search / filters | estado de la URL | searchParams (M7)
dropdown abierto | local a un componente | useState (levantar)
=== Sesion de compra (el carrito en el store, con selectores) ===
addItem(Wireless Mouse, 2599) -> count, total y items cambian:
CartBadge -> (1)
CartTotal -> $25.99
Cart -> Wireless Mouse
addItem(Free Sticker, 0) -> count e items cambian; total NO:
CartBadge -> (2)
Cart -> Wireless Mouse, Free Sticker
addItem(Mechanical Keyboard, 8900) -> count, total e items cambian:
CartBadge -> (3)
CartTotal -> $114.99
Cart -> Wireless Mouse, Free Sticker, Mechanical Keyboard
removeItem(2 = Free Sticker) -> count e items cambian; total NO:
CartBadge -> (2)
Cart -> Wireless Mouse, Mechanical Keyboard
clear() -> los tres cambian:
CartBadge -> (0)
CartTotal -> $0.00
Cart -> (vacio)
Re-renders con selectores -> CartBadge: 5 | CartTotal: 3 | Cart: 5
=== El mismo guion en Context (sin selectores): todos re-renderizan SIEMPRE ===
Re-renders con Context -> CartBadge: 5 | CartTotal: 5 | Cart: 5
CartTotal: 3 re-renders con selector vs 5 con Context.
Con selectores, el Free Sticker (total sin cambio) no toco a CartTotal.
=== products NO paso por el store: es del servidor -> React Query (M4-M6) ===
Lee la salida de principio a fin, porque en ella está el módulo entero funcionando junto.
La tabla de clasificación abre el proyecto con la decisión hecha explícita: el carrito va en el store (global de cliente, cambia seguido); el tema y el usuario en Context (cambian poco); los productos en React Query (servidor); la búsqueda en la URL; un menú abierto en useState. Cada pieza, su herramienta. Es la lección del módulo 6 aplicada a Mercado, y el mapa de qué construye este proyecto (la primera fila) y qué señala hacia adelante (las demás).
La sesión ejecuta el patrón. Sigue la columna del CartTotal, que es la reveladora. Con addItem(Mouse) el total sube a $25.99 → el CartTotal reacciona. Con addItem(Free Sticker, 0) el conteo sube a 2 y la lista crece (el CartBadge y el Cart reaccionan), pero el total sigue en $25.99 → el CartTotal no imprime nada, su columna no cambió. Con addItem(Keyboard) el total salta a $114.99 → el CartTotal reacciona. Con removeItem(Free Sticker) sale el sticker: el conteo baja a 2 y la lista cambia, pero el total sigue en $114.99 (quitar algo de $0 no lo mueve) → el CartTotal otra vez calla. Con clear() todo va a cero → los tres reaccionan. El resultado medido: CartBadge 5 re-renders, CartTotal 3, Cart 5. Cada suscriptor reaccionó solo cuando su columna cambió.
El contraste con Context cierra la tesis. El mismo guion, pero suscritos al estado entero: los tres re-renderizan en las 5 acciones. El CartTotal pasa de 3 re-renders (con selector) a 5 (con Context): dos re-renders inútiles, los del sticker gratis (agregar y quitar), donde su dato no cambió pero re-renderizó igual. Esos dos re-renders de más, por un solo componente y una sola sesión corta, son el desperdicio que el selector elimina. Multiplícalo por los componentes de un storefront real y las decenas de cambios de un carrito, y tienes la razón, medida, de todo el módulo.
Y el cierre lo dice todo: los productos nunca pasaron por el store. No son estado de cliente; son una copia en caché de datos del servidor, y su herramienta es React Query (módulos 4-6). Ese es el módulo entero en una corrida: clasificar (tabla), montar en un store lo que cambia seguido (el carrito) con el patrón correcto (estado + acciones fuera del árbol, selectores finos), y dejar fuera lo que no le corresponde (el catálogo del servidor).
Errores comunes
Meter todo el estado global en el cart store. Qué pasa: agregas al useCartStore el tema, el usuario y hasta los productos, "ya que es el store de la app". Por qué pasa: tener un store invita a centralizarlo todo. Cómo detectarlo: el cart store mezcla items (cambia seguido) con el tema (cambia poco) y con productos (del servidor). Cómo corregirlo: cada pieza en su herramienta —el tema y el usuario en Context (M2), los productos en React Query (M4-M6), y el store solo para el carrito—. Un store no es un cajón para todo el estado; es la herramienta del estado de cliente que cambia seguido.
Suscribir a los consumidores al estado entero (perder el selector). Qué pasa: escribes const cart = useCartStore() en el CartBadge, el CartTotal y el Cart, y los tres re-renderizan en cada cambio del carrito —igual que Context, el problema que viniste a resolver—. Por qué pasa: es más corto no escribir el selector. Cómo detectarlo: el CartTotal re-renderiza cuando entra un item de $0. Cómo corregirlo: cada consumidor con su selector fino (selectCount, selectTotal, selectItems), y los que solo escriben (ProductCard) con la acción (s => s.addItem). El selector es la razón de haber elegido un store; sin él, no ganaste nada.
Guardar count/total en el store en vez de derivarlos. Qué pasa: además de items, guardas count y total como rebanadas, y tienes que actualizarlos en cada acción —hasta que un día addItem sube items pero olvida subir count, y el badge miente—. Por qué pasa: parece cómodo tener los números listos. Cómo detectarlo: el CartBadge muestra un número que no coincide con la cantidad real de items. Cómo corregirlo: guarda solo items y deriva count/total en los selectores (L3, L5). Una fuente, imposible de desincronizar.
Ejercicios
Ejercicio 1 — Agrega "vaciar y avisar". Quieres una acción checkout() que vacíe el carrito y, además, guarde el número de items comprados en una rebanada lastPurchaseCount (para mostrar "Compraste 3 productos"). (a) Escribe la acción. (b) ¿Qué selector usaría un componente PurchaseConfirmation que muestra ese número? (c) ¿Por qué lastPurchaseCount sí se guarda (a diferencia de count, que se deriva)?
Ver solución
(a)
checkout: () => set((s) => ({
lastPurchaseCount: s.items.length, // guarda cuantos habia ANTES de vaciar
items: [], // vacia el carrito
})),
(Y el estado inicial del store incluiría lastPurchaseCount: 0.)
(b) useCartStore((s) => s.lastPurchaseCount) — un primitivo, re-render solo cuando cambia.
(c) count se deriva de items (items.length), así que guardarlo sería redundante y desincronizable. Pero lastPurchaseCount no se puede derivar de items: es un dato que captura un momento del pasado (cuántos había antes de vaciar), y tras el clear el items está vacío. Como no es función del estado actual, es estado base y se guarda. La regla: deriva lo que sea función del estado actual; guarda lo que no puedas recalcular de él.
Ejercicio 2 — Predice los re-renders. Con el store del proyecto y los tres suscriptores (CartBadge→count, CartTotal→total, Cart→items), corres: (1) addItem(A, 1000), (2) addItem(B, 0), (3) removeItem(A), (4) addItem(C, 500). ¿Cuántas veces re-renderiza cada suscriptor?
Ver solución
Sigamos el count, el total y los items en cada paso:
- (1) addItem(A, 1000): count 0→1, total 0→1000, items cambian → los tres reaccionan.
- (2) addItem(B, 0): count 1→2, total 1000→1000 (B es gratis), items cambian →
CartBadgeyCartsí,CartTotalno. - (3) removeItem(A): count 2→1, total 1000→0 (sale A, queda B a $0), items cambian → los tres reaccionan.
- (4) addItem(C, 500): count 1→2, total 0→500, items cambian → los tres reaccionan.
Totales: CartBadge → 4 (el count cambió en los 4 pasos), CartTotal → 3 (el total cambió en 1, 3, 4; no en 2), Cart → 4 (los items cambiaron en los 4). Con Context, los tres re-renderizarían 4 veces: el CartTotal tendría 1 re-render inútil (el del paso 2).
Ejercicio 3 — ¿Qué lección resuelve qué? El proyecto toca todo el módulo. Para cada pieza, di de qué lección es el concepto de fondo: (a) el useCartStore declarado en el módulo, fuera de todo componente; (b) addItem haciendo [...s.items, product] en vez de push; (c) el CartTotal que no re-renderiza cuando entra un item de $0; (d) el ProductCard que lee s => s.addItem y no re-renderiza por el carrito; (e) los productos quedándose fuera del store.
Ver solución
- (a)
useCartStorefuera de todo componente — el store vive fuera de React (L2): una fuente única de verdad en el ámbito del módulo, que cualquier componente lee sin props ni Provider. - (b)
addItemsin mutar — estado y acciones juntos (L3): las acciones producen estado nuevo (identidad distinta) para que el store detecte el cambio y avise. - (c)
CartTotalque calla con un item de $0 — suscribirse con un selector (L4): el selector compara la rebanada (el total); si no cambió, no re-renderiza. - (d)
ProductCardcons => s.addItem— selectores en la práctica / acciones estables (L3, L5): las acciones tienen identidad estable, así que un componente que solo despacha no re-renderiza por el estado. - (e) productos fuera del store — store vs Context vs levantar (L6): el estado del servidor no es de cliente; va en React Query (M4-M6), no en el store.
El proyecto integra el módulo: clasificar (L6), montar el store fuera del árbol (L2), acciones sin mutar (L3), selectores finos (L4-L5), aplicar a Mercado (L7). Y ninguna pieza necesitó meter en el store lo que no le tocaba.
Resumen y siguiente paso
En este mini-proyecto manejaste el carrito de Mercado de punta a punta con un store. Empezaste con la tabla de clasificación —cart → store; tema/usuario → Context (M2); productos → React Query (M4-M6); búsqueda → URL (M7); menú → local—, que hace explícita la decisión antes de escribir código. Montaste el carrito como un store con items + addItem/removeItem/clear y los selectores selectCount/selectTotal/selectItems, fuera del árbol, con cada componente suscrito a su rebanada. Y lo ejecutaste en una sesión de compra con tres suscriptores, donde cada uno reaccionó solo a su columna —el CartTotal calló con los items de $0 (3 re-renders con selector vs 5 con Context)— y los productos quedaron deliberadamente fuera del store. Ese es el módulo entero funcionando junto: clasificar, montar en un store lo que cambia seguido con el patrón correcto (estado + acciones fuera del árbol, selectores finos), y dejar fuera lo que no le corresponde.
Antes de cerrar el módulo deberías poder: clasificar el estado de una app y ubicar el carrito en un store; montar un store con estado, acciones (sin mutar) y selectores fuera del árbol; suscribir cada componente a la rebanada mínima que usa (incluida la acción, para los que solo escriben); y explicar por qué los productos, el tema y la búsqueda no van en el store.
Y hacia dónde sigue la guía. Con este módulo terminaste el estado global de cliente: lo estable en Context (M2), lo que cambia seguido en un store (M3). Pero en cada lección apareció, señalada y dejada fuera, la caja más importante de todas: el estado del servidor —los productos de Mercado, que no son tuyos, sino una copia en caché de datos que viven en el backend—. El módulo 4 abre esa caja: por qué el estado del servidor es distinto (se pone stale, hay que refetchearlo, se comparte entre usuarios, puede fallar), y por qué manejarlo con useState + useEffect —o metiéndolo en un store, como acabas de aprender a no hacer— está mal. Es el corazón de la guía, y la razón de su tesis: la mayoría de los "problemas de estado" son estado del servidor mal clasificado. Después vendrán React Query (M5), las mutaciones (M6) y la URL (M7), hasta que cada caja tenga su herramienta.
Recursos
- Zustand, documentación oficial — docs.pmnd.rs/zustand/getting-started/introduction. La referencia completa del
create, las acciones y los selectores que arma este proyecto. En inglés. - TkDodo, "Working with Zustand" — tkdodo.eu/blog/working-with-zustand. Las buenas prácticas que este mini-proyecto aplica: store fuera del árbol, acciones estables, selectores atómicos, y no meter estado del servidor. En inglés.
- React, "Scaling Up with Reducer and Context" — react.dev/learn/scaling-up-with-reducer-and-context. El patrón de estado global con React puro, para contrastar con el store y entender qué gana con los selectores. En inglés.
- TanStack Query, "Overview" — tanstack.com/query/latest/docs/framework/react/overview. El destino de los productos que este proyecto deja fuera del store: el estado del servidor y su herramienta, que abre el módulo 4. En inglés.