Módulo 8: Project Build Mercados Storefront
Proyecto: el storefront de Mercado, completo
Descripción
Este es el entregable. Durante seis lecciones armaste el storefront de Mercado por capas —el andamiaje del árbol (2), la carga con un efecto (3), la búsqueda controlada y derivada (4), el carrito levantado con su reducer (5), los callbacks que conectan todo (6) y el pulido (7)—. Ahora las juntas en un solo entregable: el storefront completo, con su árbol de componentes, su código React real, y la lógica ejecutada en Node que prueba que funciona de punta a punta. Esta lección es a la vez el proyecto final del módulo y el cierre de toda la guía: cuando la termines, habrás construido una app React real que integra los ocho módulos, y tendrás el mapa de hacia dónde seguir.
Conexión con el módulo. Es el capstone del capstone: no hay concepto nuevo, hay síntesis. Cada módulo de la guía aparece en el App final: componentes y composición (M1), JSX con listas y key (M2), estado con useState (M3), eventos e inputs controlados (M4), la lista derivada (M5), la carga con useEffect (M6), y el carrito levantado con useReducer (M7). En vez de "Errores comunes", esta lección te da una rúbrica para autoevaluarte y ejercicios de transferencia que estiran lo aprendido a requisitos nuevos —porque la prueba de que dominas los fundamentos no es repetir el storefront, sino saber extenderlo—.
El enunciado
Construye el storefront de Mercado: una tienda de una sola pantalla con búsqueda, catálogo y carrito. Requisitos, cada uno atado a su módulo:
- Componentes y composición (M1-M2). Descompón la pantalla en seis componentes —
App,SearchBar,ProductList,ProductCard,Cart,CartItem— y compónlos en un árbol. Cada lista se renderiza con.map()ykey={id}. - Carga de datos (M6). El
Appcarga susproductsal montar con unuseEffect(fetch simulado). Mientrasproductsseanull, muestraLoading...; el efecto lleva cleanup. - Búsqueda controlada + lista derivada (M3, M4, M5). La
SearchBares un input controlado cuyoqueryes estado en elApp. La lista visible se deriva deproducts+querycongetVisibleProducts(filtra por la búsqueda, ordena por precio) —no se guarda—. - Carrito levantado con reducer (M3, M7). El
cartvive en elApp(ancestro común deProductListyCart), manejado por elcartReducerpuro (add/remove/clear) víauseReducer. - Callbacks que suben (M4, M7).
onAddToCart(desdeProductCard),onRemoveFromCart(desdeCartItem) yonSearch(desdeSearchBar) suben alApp, que actualiza el estado. Los hijos avisan; elAppdecide. - Pulido (M2, M5).
key={id}estable en las listas, y estados vacíos: "No products match your search" y "Your cart is empty".
La entrega esperada son tres cosas: el árbol de componentes (el diagrama), el código React real (la solución de referencia), y la lógica ejecutada (el reducer + la derivación + el total corriendo una sesión, y el storefront renderizado a HTML).
El árbol de componentes
flowchart TD
App["App<br/>products (useState, carga con useEffect)<br/>query (useState) · cart (useReducer)"]
App -- "query" --> SB["SearchBar<br/>input controlado"]
App -- "products = getVisibleProducts(...)" --> PL["ProductList"]
App -- "items = cart" --> Cart["Cart"]
PL --> PC1["ProductCard"]
PL --> PC2["ProductCard"]
Cart --> CI1["CartItem"]
Cart --> CI2["CartItem"]
SB -. "onSearch(text)" .-> App
PC1 -. "onAddToCart(product)" .-> App
CI1 -. "onRemoveFromCart(id)" .-> App
Las flechas continuas son props que bajan; las punteadas, callbacks que suben. El App es el único con estado: products (cargado), query (búsqueda) y cart (carrito). La lista que baja a ProductList está derivada de products y query; el cart baja al Cart tal cual. Un solo lugar tiene el estado; los hijos leen y avisan.
Ejemplo trabajado: la lógica del storefront, ejecutada
Verifiquemos que todo funciona corriendo la lógica pura en Node —el cartReducer, la derivación con getVisibleProducts, el cartTotal— junto con el mini renderToString que pinta el storefront entero. Es una sesión real: el storefront se monta, el usuario busca "e", agrega productos (subiendo una cantidad), quita uno, y vemos el HTML final. La lógica de negocio va completa; el renderToString y los seis componentes h(...) son exactamente los de la lección 2:
// ---- La logica pura del storefront (los tres pilares, completos) ----
function formatPrice(cents) { return '$' + (cents / 100).toFixed(2); }
function cartTotal(items) { return items.reduce((s, l) => s + l.priceCents * l.qty, 0); }
function getVisibleProducts(products, query) { // leccion 4 (M5): derivar la lista
const q = query.trim().toLowerCase();
return products
.filter((p) => p.name.toLowerCase().includes(q))
.sort((a, b) => a.priceCents - b.priceCents);
}
function cartReducer(state, action) { // leccion 5 (M7): la logica del carrito
switch (action.type) {
case 'add': {
const line = state.find((item) => item.id === action.product.id);
if (line) return state.map((item) => item.id === action.product.id ? { ...item, qty: item.qty + 1 } : item);
return [...state, { ...action.product, qty: 1 }];
}
case 'remove': {
const line = state.find((item) => item.id === action.id);
if (line && line.qty > 1) return state.map((item) => item.id === action.id ? { ...item, qty: item.qty - 1 } : item);
return state.filter((item) => item.id !== action.id);
}
case 'clear': return [];
default: return state;
}
}
// renderToString(...) y los seis componentes h(...) [App, SearchBar, ProductList,
// ProductCard, Cart, CartItem]: EXACTAMENTE como en la leccion 2. App({ products, query, cart })
// deriva la lista con getVisibleProducts antes de bajarla al ProductList.
const PRODUCTS = [
{ id: 'p1', name: 'Wireless Mouse', priceCents: 2599, category: 'peripherals', inStock: true },
{ id: 'p2', name: 'Mechanical Keyboard', priceCents: 8900, category: 'peripherals', inStock: false },
{ id: 'p3', name: 'USB-C Hub', priceCents: 3499, category: 'peripherals', inStock: true },
{ id: 'p4', name: 'Laptop Stand', priceCents: 4500, category: 'furniture', inStock: true },
{ id: 'p5', name: 'Desk Lamp', priceCents: 1999, category: 'furniture', inStock: true },
];
// El estado del App: query (useState) + cart (useReducer). Aqui, variables planas.
let query = '';
let cart = [];
const MOUSE = PRODUCTS[0];
const HUB = PRODUCTS[2];
function cartLine() { return `[${cart.map((i) => `${i.name} x${i.qty}`).join(', ')}] total ${formatPrice(cartTotal(cart))}`; }
console.log('=== 1) Montaje del storefront (query="", carrito vacio) ===');
console.log(renderToString(App({ products: PRODUCTS, query, cart })));
console.log('\n=== 2) El usuario busca "e": la lista visible se DERIVA ===');
query = 'e';
const visible = getVisibleProducts(PRODUCTS, query);
console.log(`query="e" -> ${visible.length} visibles: ` + visible.map((p) => `${p.name} ${formatPrice(p.priceCents)}`).join(' | '));
console.log('\n=== 3) El carrito: cada accion la decide el cartReducer ===');
console.log(`${'inicial'.padEnd(22)}${cartLine()}`);
cart = cartReducer(cart, { type: 'add', product: MOUSE }); console.log(`${'add Wireless Mouse'.padEnd(22)}${cartLine()}`);
cart = cartReducer(cart, { type: 'add', product: HUB }); console.log(`${'add USB-C Hub'.padEnd(22)}${cartLine()}`);
cart = cartReducer(cart, { type: 'add', product: MOUSE }); console.log(`${'add Wireless Mouse'.padEnd(22)}${cartLine()}`);
cart = cartReducer(cart, { type: 'remove', id: MOUSE.id }); console.log(`${'remove Wireless Mouse'.padEnd(22)}${cartLine()}`);
console.log('\n=== 4) Estado final del storefront (query="e", carrito poblado) ===');
console.log(renderToString(App({ products: PRODUCTS, query, cart })));
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== 1) Montaje del storefront (query="", carrito vacio) ===
<main class="storefront">
<div class="search-bar">
<label for="product-search">Search products</label>
<input id="product-search" type="search" class="search-input" placeholder="Search products..." value="" />
</div>
<section class="product-list">
<article class="product-card">
<h3 class="product-name">Desk Lamp</h3>
<p class="product-price">$19.99</p>
<span class="product-stock">In stock</span>
<button class="add-btn">Add to cart</button>
</article>
<article class="product-card">
<h3 class="product-name">Wireless Mouse</h3>
<p class="product-price">$25.99</p>
<span class="product-stock">In stock</span>
<button class="add-btn">Add to cart</button>
</article>
<article class="product-card">
<h3 class="product-name">USB-C Hub</h3>
<p class="product-price">$34.99</p>
<span class="product-stock">In stock</span>
<button class="add-btn">Add to cart</button>
</article>
<article class="product-card">
<h3 class="product-name">Laptop Stand</h3>
<p class="product-price">$45.00</p>
<span class="product-stock">In stock</span>
<button class="add-btn">Add to cart</button>
</article>
<article class="product-card">
<h3 class="product-name">Mechanical Keyboard</h3>
<p class="product-price">$89.00</p>
<span class="product-stock">Out of stock</span>
<span class="badge badge-out">Sold out</span>
<button class="add-btn" disabled>Add to cart</button>
</article>
</section>
<aside class="cart">
<h2 class="cart-title">Your cart</h2>
<p class="empty-state">Your cart is empty</p>
<p class="cart-total">Total: $0.00</p>
</aside>
</main>
=== 2) El usuario busca "e": la lista visible se DERIVA ===
query="e" -> 3 visibles: Desk Lamp $19.99 | Wireless Mouse $25.99 | Mechanical Keyboard $89.00
=== 3) El carrito: cada accion la decide el cartReducer ===
inicial [] total $0.00
add Wireless Mouse [Wireless Mouse x1] total $25.99
add USB-C Hub [Wireless Mouse x1, USB-C Hub x1] total $60.98
add Wireless Mouse [Wireless Mouse x2, USB-C Hub x1] total $86.97
remove Wireless Mouse [Wireless Mouse x1, USB-C Hub x1] total $60.98
=== 4) Estado final del storefront (query="e", carrito poblado) ===
<main class="storefront">
<div class="search-bar">
<label for="product-search">Search products</label>
<input id="product-search" type="search" class="search-input" placeholder="Search products..." value="e" />
</div>
<section class="product-list">
<article class="product-card">
<h3 class="product-name">Desk Lamp</h3>
<p class="product-price">$19.99</p>
<span class="product-stock">In stock</span>
<button class="add-btn">Add to cart</button>
</article>
<article class="product-card">
<h3 class="product-name">Wireless Mouse</h3>
<p class="product-price">$25.99</p>
<span class="product-stock">In stock</span>
<button class="add-btn">Add to cart</button>
</article>
<article class="product-card">
<h3 class="product-name">Mechanical Keyboard</h3>
<p class="product-price">$89.00</p>
<span class="product-stock">Out of stock</span>
<span class="badge badge-out">Sold out</span>
<button class="add-btn" disabled>Add to cart</button>
</article>
</section>
<aside class="cart">
<h2 class="cart-title">Your cart</h2>
<ul class="cart-items">
<li class="cart-item">
<span class="cart-item-name">Wireless Mouse</span>
<span class="cart-item-qty">x1</span>
<span class="cart-item-line">$25.99</span>
<button class="remove-btn">Remove</button>
</li>
<li class="cart-item">
<span class="cart-item-name">USB-C Hub</span>
<span class="cart-item-qty">x1</span>
<span class="cart-item-line">$34.99</span>
<button class="remove-btn">Remove</button>
</li>
</ul>
<p class="cart-total">Total: $60.98</p>
</aside>
</main>
Esta es la prueba de que el storefront funciona, y con ella se cierra la promesa de toda la guía: nada citado de memoria, todo ejecutado. Recórrela por partes.
Parte 1 — el montaje. El HTML del storefront entero, con query="" y el carrito vacío. La lista sale derivada y ordenada por precio (Desk Lamp $19.99 primero, Mechanical Keyboard $89.00 último) —no en el orden del array—, con el teclado agotado mostrando su badge "Sold out" y su botón disabled. El carrito muestra su estado vacío ("Your cart is empty", $0.00). Es la app recién cargada.
Parte 2 — la búsqueda. Con query="e", la derivación recorta la lista a 3 productos (los que contienen "e"), ordenados por precio. La lista se computó de products y query; nada se guardó.
Parte 3 — el carrito. La sesión de acciones, decidida por el cartReducer: agregar el mouse (x1), agregar el hub, agregar el mouse otra vez (sube a x2, no duplica), quitar el mouse (baja a x1). El total en centavos cuadró en cada paso —$25.99, $60.98, $86.97, $60.98—.
Parte 4 — el estado final. El HTML del storefront con todo integrado a la vez: la search-bar reflejando value="e" (input controlado), la product-list con los 3 productos derivados de la búsqueda "e", y el cart con sus dos líneas (Wireless Mouse x1 a $25.99, USB-C Hub x1 a $34.99) y el total $60.98. Búsqueda, derivación, carrito y render, todo en una sola foto coherente. Ese HTML es el storefront de Mercado, construido con los ocho módulos y verificado ejecutando su lógica.
La solución de referencia
Este es el App completo en React real: la respuesta al enunciado, integrando los ocho módulos. Compárala con la tuya después de intentarlo.
Ver la solución de referencia completa (el storefront entero en React)
import { useEffect, useReducer, useState } from 'react';
// ---------- Logica pura (fuera de los componentes: reusable y testeable) ----------
function formatPrice(cents) {
return '$' + (cents / 100).toFixed(2);
}
function cartTotal(items) {
return items.reduce((sum, line) => sum + line.priceCents * line.qty, 0);
}
function getVisibleProducts(products, query) {
const q = query.trim().toLowerCase();
return products
.filter((p) => p.name.toLowerCase().includes(q)) // filtra por la busqueda
.sort((a, b) => a.priceCents - b.priceCents); // ordena por precio ascendente
}
function cartReducer(state, action) {
switch (action.type) {
case 'add': {
const line = state.find((item) => item.id === action.product.id);
if (line) {
return state.map((item) =>
item.id === action.product.id ? { ...item, qty: item.qty + 1 } : item);
}
return [...state, { ...action.product, qty: 1 }];
}
case 'remove': {
const line = state.find((item) => item.id === action.id);
if (line && line.qty > 1) {
return state.map((item) =>
item.id === action.id ? { ...item, qty: item.qty - 1 } : item);
}
return state.filter((item) => item.id !== action.id);
}
case 'clear':
return [];
default:
return state;
}
}
// ---------- Componentes presentacionales (funciones puras de sus props) ----------
function SearchBar({ query, onSearch }) {
return (
<div className="search-bar">
<label htmlFor="product-search">Search products</label>
<input
id="product-search"
type="search"
className="search-input"
placeholder="Search products..."
value={query} // input controlado (M4)
onChange={(e) => onSearch(e.target.value)} // sube el texto al App
/>
</div>
);
}
function ProductCard({ product, onAddToCart }) {
return (
<article className="product-card">
<h3 className="product-name">{product.name}</h3>
<p className="product-price">{formatPrice(product.priceCents)}</p>
<span className="product-stock">{product.inStock ? 'In stock' : 'Out of stock'}</span>
{!product.inStock && <span className="badge badge-out">Sold out</span>}
<button
className="add-btn"
disabled={!product.inStock}
onClick={() => onAddToCart(product)} // avisa hacia arriba (M4)
>
Add to cart
</button>
</article>
);
}
function ProductList({ products, onAddToCart }) {
if (products.length === 0) {
return <p className="empty-state">No products match your search</p>; // estado vacio (M2/M5)
}
return (
<section className="product-list">
{products.map((product) => (
<ProductCard key={product.id} product={product} onAddToCart={onAddToCart} /> // key = id (M2)
))}
</section>
);
}
function CartItem({ line, onRemoveFromCart }) {
return (
<li className="cart-item">
<span className="cart-item-name">{line.name}</span>
<span className="cart-item-qty">{'x' + line.qty}</span>
<span className="cart-item-line">{formatPrice(line.priceCents * line.qty)}</span>
<button className="remove-btn" onClick={() => onRemoveFromCart(line.id)}>Remove</button>
</li>
);
}
function Cart({ items, onRemoveFromCart, onClear }) {
return (
<aside className="cart">
<h2 className="cart-title">Your cart</h2>
{items.length === 0 ? (
<p className="empty-state">Your cart is empty</p>
) : (
<ul className="cart-items">
{items.map((line) => (
<CartItem key={line.id} line={line} onRemoveFromCart={onRemoveFromCart} />
))}
</ul>
)}
<p className="cart-total">Total: {formatPrice(cartTotal(items))}</p>
</aside>
);
}
// ---------- El container: el App, dueño de TODO el estado ----------
function App() {
const [products, setProducts] = useState(null); // M6: carga (null = Loading)
const [query, setQuery] = useState(''); // M3-M4: la busqueda
const [cart, dispatch] = useReducer(cartReducer, []); // M7: el carrito levantado
// M6: cargar los productos al montar, con cleanup
useEffect(() => {
let active = true;
fetchProducts().then((data) => {
if (active) setProducts(data); // solo guardo si el App sigue montado
});
return () => { active = false; };
}, []);
if (products === null) {
return <p className="loading">Loading...</p>; // patron de fetch (M6)
}
// M5: la lista visible se DERIVA de products + query (no se guarda)
const visible = getVisibleProducts(products, query);
return (
<main className="storefront">
<SearchBar query={query} onSearch={setQuery} />
<ProductList
products={visible}
onAddToCart={(product) => dispatch({ type: 'add', product })}
/>
<Cart
items={cart}
onRemoveFromCart={(id) => dispatch({ type: 'remove', id })}
onClear={() => dispatch({ type: 'clear' })}
/>
</main>
);
}
Léela como el mapa de la guía. El App (container) tiene todo el estado: products cargado con useEffect (M6), query con useState (M3), cart con useReducer (M7). Deriva visible en el render (M5) y baja datos + callbacks a los hijos (M1, M4). Los seis componentes son presentacionales: funciones puras de sus props, sin estado propio (M1, M7). Cada línea toca un módulo, y juntas son el storefront.
La rúbrica
Autoevalúa tu storefront contra estos criterios. Cada uno mapea a un módulo; si los ocho están, integraste la guía:
| # | Criterio | Módulo | ¿Cumple? |
|---|---|---|---|
| 1 | Seis componentes con responsabilidad única, compuestos en un árbol; datos que bajan por props | M1 | ☐ |
| 2 | Cada lista con .map() y key={id} (no el índice); condicionales bien hechos (&&, ternario, early return) | M2 | ☐ |
| 3 | query y cart son estado en el App; no se muta el estado (estructuras nuevas) | M3 | ☐ |
| 4 | SearchBar es input controlado (value + onChange); onAddToCart/onRemoveFromCart/onSearch suben por callbacks (con la flecha) | M4 | ☐ |
| 5 | La lista visible se deriva con getVisibleProducts (no se guarda); el total y "¿vacío?" también | M5 | ☐ |
| 6 | Los products se cargan con useEffect (null → Loading...), con cleanup | M6 | ☐ |
| 7 | El cart está levantado en el App; su lógica vive en el cartReducer puro conectado con useReducer | M7 | ☐ |
| 8 | Estados vacíos ("No products match…", "Your cart is empty") manejados; nada de pantalla en blanco | M2/M5 | ☐ |
Si alguno queda sin marcar, ya sabes a qué módulo volver. La rúbrica no mide "cuánto código escribiste": mide que cada herramienta esté en su sitio, aplicada por la razón correcta.
Ejercicios de transferencia
La prueba de que dominas los fundamentos no es repetir el storefront, sino extenderlo. Estos tres ejercicios estiran lo aprendido a requisitos nuevos.
Ejercicio 1 — El badge de conteo en el header. Mercado quiere un CartBadge en un Header (arriba a la izquierda) que muestre cuántos items hay en el carrito —contando cantidades: Wireless Mouse x2 cuenta como 2—. El Header y el ProductList están en ramas distintas del árbol. ¿Dónde vive el conteo, es estado o derivado, y cómo llega al CartBadge?
Ver solución
El conteo no vive en ningún lado como estado: es derivado del cart. Y el cart ya está levantado en el App (M7), que es el ancestro común del Header (donde va el badge) y del ProductList (donde se agrega). Así que:
function App() {
// ... products, query, cart como en la solucion ...
const itemCount = cart.reduce((n, line) => n + line.qty, 0); // DERIVADO del cart
return (
<>
<Header><CartBadge count={itemCount} /></Header>
<main className="storefront">
<SearchBar query={query} onSearch={setQuery} />
<ProductList products={visible} onAddToCart={(p) => dispatch({ type: 'add', product: p })} />
<Cart items={cart} onRemoveFromCart={(id) => dispatch({ type: 'remove', id })} />
</main>
</>
);
}
function CartBadge({ count }) { return <span className="cart-badge">{count}</span>; }
itemCount se deriva de cart con un reduce que suma las cantidades (no cart.length, que contaría líneas, no unidades). Baja al CartBadge por props. Como el cart es la única fuente de verdad levantada en el App, el badge y el Cart siempre concuerdan: agregas un producto y los dos suben a la vez. No hace falta estado nuevo ni Context —es lifting + derivación, lo de esta guía—.
Ejercicio 2 — El filtro "solo disponibles". Agrega un checkbox "Only in stock" que, cuando está marcado, oculta los productos agotados. Di si onlyInStock es estado o derivado, dónde vive, y cómo cambia getVisibleProducts.
Ver solución
onlyInStock es estado (un useState(false) en el App): lo controla el usuario con el checkbox, no se deduce de nada. La lista filtrada por él sigue siendo derivada —es parte de getVisibleProducts—:
function getVisibleProducts(products, query, onlyInStock) {
const q = query.trim().toLowerCase();
return products
.filter((p) => p.name.toLowerCase().includes(q))
.filter((p) => !onlyInStock || p.inStock) // nuevo criterio: solo disponibles
.sort((a, b) => a.priceCents - b.priceCents);
}
function App() {
const [query, setQuery] = useState('');
const [onlyInStock, setOnlyInStock] = useState(false); // ESTADO nuevo
// ...
const visible = getVisibleProducts(products, query, onlyInStock); // DERIVADO
// ... baja <StockFilter checked={onlyInStock} onChange={setOnlyInStock} /> ...
}
onlyInStock vive en el App, junto a query, porque el ProductList necesita ambos para derivar. Un criterio nuevo se suma a la cadena de derivación (getVisibleProducts); no se convierte en una lista guardada. Con el checkbox marcado, Mechanical Keyboard (inStock: false) desaparece de la vista —sin tocar products, solo derivando distinto—. Es el mismo patrón del módulo 5: guarda el criterio (estado), deriva la lista (consecuencia).
Ejercicio 3 — Mover la búsqueda al servidor. Hoy la búsqueda filtra en el cliente sobre los productos ya cargados. Mercado crece y quiere buscar en el servidor (el catálogo es enorme). ¿Qué pieza deja de ser una derivación y pasa a ser un efecto? ¿Cómo cambia el array de dependencias? ¿A qué guía hermana te lleva esto?
Ver solución
La búsqueda deja de ser una derivación y pasa a ser un efecto. Hoy getVisibleProducts(products, query) filtra en el cliente, en el render (M5): sin efecto. Si la búsqueda es en el servidor, cada cambio de query debe re-pedir al servidor (tocar la red = efecto, M6), y query entra al array de dependencias:
function App() {
const [query, setQuery] = useState('');
const [products, setProducts] = useState(null);
useEffect(() => {
let active = true;
searchProducts(query).then((data) => { if (active) setProducts(data); }); // el servidor filtra
return () => { active = false; };
}, [query]); // re-pide cuando query cambia (antes era [])
// Ya NO derivamos: products viene filtrado del servidor.
if (products === null) return <p className="loading">Loading...</p>;
return (/* ... ProductList products={products} ... */);
}
Dos cambios: (1) el efecto pasa de [] (cargar una vez) a [query] (re-pedir en cada búsqueda), y necesita cleanup race-safe porque las respuestas pueden llegar fuera de orden (M6, la protección active); (2) desaparece getVisibleProducts —el servidor ya filtra, no hay nada que derivar en el cliente—.
Esto te lleva de lleno a las guías hermanas: el manejo crudo de carga/error/caché/race que ahora harías a mano es lo que resuelve una librería de datos (React Query, SWR → frontend-state-and-data), y el fetch en el servidor sin useEffect ni Loading... es nextjs-app-router. El mismo requisito ("buscar") es derivación o efecto según dónde ocurra el filtrado —cliente o servidor—; distinguirlo es el criterio que te llevas de esta guía.
El cierre de la guía: los ocho módulos, un método
Terminaste la Guía de Fundamentos de React. Vale la pena mirar atrás y ver lo que construiste, porque no fueron ocho temas: fue un método, aprendido pieza por pieza y ensamblado en este storefront.
- M1 — Pensar en componentes. Aprendiste a ver una pantalla como un árbol de piezas (
UI = f(props)) y a componerlas. - M2 — JSX y renderizado. A describir la UI declarativamente: expresiones, atributos, condicionales, listas con
key. - M3 — Estado con
useState. A darle memoria a un componente y entender el estado como snapshot que dispara re-render. - M4 — Eventos y handlers. A conectar al usuario: inputs controlados y callbacks que suben datos.
- M5 — Estado derivado y listas. A derivar en vez de guardar, manteniendo el estado fuente mínimo.
- M6 — Efectos. A sincronizar con el afuera con
useEffect, y —tan importante— a saber cuándo no usarlo. - M7 — Levantar el estado y composición. A poner el estado compartido en el ancestro común y consolidar lógica en un reducer.
- M8 — El storefront. A integrar los siete en una app real, construida por capas y verificada.
La tesis que cosió todo: la UI es una función del estado. Describes cómo se ve la interfaz para un estado dado, React pinta el DOM, y cuando el estado cambia, la UI se recalcula sola. Todo lo demás —componentes, JSX, hooks, eventos, derivación, efectos, lifting— son las herramientas para hacer esa función bien: dónde poner el estado, cómo cambiarlo, qué derivar, cuándo tocar el afuera. Con eso puedes construir una app React real. Ya lo hiciste.
Hacia dónde seguir
Esta guía te dio los fundamentos de React del lado del cliente. El ecosistema Fullstack sigue desde aquí, y este storefront te deja en la puerta de cada guía hermana:
nextjs-app-router— Lleva el storefront a producción: routing (URLs, páginas compartibles), SSR/SSG y Server Components, y data fetching en el servidor (sin eluseEffect+Loading...que hiciste a mano). El siguiente paso natural.frontend-state-and-data— El estado a escala: Context (para el prop drilling que la composición ya no cubre), estado global (Zustand/Redux), y datos del servidor con React Query/SWR (caché, mutaciones, race) —lo que aquí resolviste crudo conuseEffect—.ui-systems-and-design-implementation— El sistema visual: Tailwind, design systems, tokens y componentes. LasclassNameque aquí fueron marcadores (product-card,badge-out) se vuelven un sistema de diseño real.fullstack-performance-and-deployment— Hacer el storefront rápido y publicable:memoy optimización de re-renders, code-splitting, build y deploy.
Y las bases previas, si necesitas reforzarlas: web-fundamentals-html-css (HTML/CSS, box-model, flexbox), que sostiene todo lo que renderizas.
Resumen y cierre
En este proyecto final integraste toda la guía construyendo el storefront de Mercado de punta a punta: el árbol de seis componentes (M1-M2), el query y el cart como estado en el App (M3), la SearchBar controlada y los callbacks que suben (M4), la lista derivada con getVisibleProducts (M5), la carga con useEffect y cleanup (M6), y el cart levantado con el cartReducer (M7). Lo entregaste con las tres piezas de un capstone —el árbol, el código React real, y la lógica ejecutada—: viste el cartReducer correr una sesión con el total en centavos cuadrando, la lista derivarse de la búsqueda, y el storefront entero renderizado a HTML en el montaje y en el estado final. Te autoevaluaste con la rúbrica y estiraste lo aprendido con tres ejercicios de transferencia (el badge derivado, el filtro nuevo, la búsqueda en el servidor).
Con esto cierras la Guía de Fundamentos de React. Sabes construir una app React real: descomponer en componentes, describir con JSX, manejar estado, responder a eventos, derivar en vez de guardar, sincronizar con efectos, y levantar el estado compartido —todo con el criterio de cuándo usar cada herramienta y cuándo no—. El siguiente paso es llevar esto a producción: nextjs-app-router para el routing y el servidor, frontend-state-and-data para el estado a escala, ui-systems-and-design-implementation para el diseño, y fullstack-performance-and-deployment para el rendimiento y el deploy. Construiste el storefront; ahora tienes el mapa para llevarlo al mundo.
Recursos
- React, "Thinking in React" — react.dev/learn/thinking-in-react. El método de cinco pasos que este storefront sigue: descomponer, versión estática, estado mínimo, dónde vive, conectar eventos. El resumen ejecutivo de la guía entera. En inglés.
- React, "Tutorial: Tic-Tac-Toe" — react.dev/learn/tutorial-tic-tac-toe. El tutorial oficial que construye una app completa integrando componentes, estado, eventos y levantar el estado —otra app de punta a punta para practicar la integración—. En inglés.
- React, "Escape Hatches" — react.dev/learn/escape-hatches. La sección sobre
useEffect, refs y sincronización con sistemas externos; profundiza la carga de datos y el criterio de cuándo salir del modelo declarativo. Adónde ir después de los fundamentos. En inglés. - React, "Managing State" — react.dev/learn/managing-state. El repaso completo de estado derivado, levantar el estado, reducers y Context —lo que el storefront integra y el puente a
frontend-state-and-data—. En inglés. - MDN, "JavaScript reference — Array methods" — developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array.
map,filter,reduce,sort, spread: las operaciones inmutables que sostienen la derivación y el reducer del storefront. En inglés.