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:

  1. 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() y key={id}.
  2. Carga de datos (M6). El App carga sus products al montar con un useEffect (fetch simulado). Mientras products sea null, muestra Loading...; el efecto lleva cleanup.
  3. Búsqueda controlada + lista derivada (M3, M4, M5). La SearchBar es un input controlado cuyo query es estado en el App. La lista visible se deriva de products + query con getVisibleProducts (filtra por la búsqueda, ordena por precio) —no se guarda—.
  4. Carrito levantado con reducer (M3, M7). El cart vive en el App (ancestro común de ProductList y Cart), manejado por el cartReducer puro (add/remove/clear) vía useReducer.
  5. Callbacks que suben (M4, M7). onAddToCart (desde ProductCard), onRemoveFromCart (desde CartItem) y onSearch (desde SearchBar) suben al App, que actualiza el estado. Los hijos avisan; el App decide.
  6. 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:

#CriterioMódulo¿Cumple?
1Seis componentes con responsabilidad única, compuestos en un árbol; datos que bajan por propsM1
2Cada lista con .map() y key={id} (no el índice); condicionales bien hechos (&&, ternario, early return)M2
3query y cart son estado en el App; no se muta el estado (estructuras nuevas)M3
4SearchBar es input controlado (value + onChange); onAddToCart/onRemoveFromCart/onSearch suben por callbacks (con la flecha)M4
5La lista visible se deriva con getVisibleProducts (no se guarda); el total y "¿vacío?" tambiénM5
6Los products se cargan con useEffect (nullLoading...), con cleanupM6
7El cart está levantado en el App; su lógica vive en el cartReducer puro conectado con useReducerM7
8Estados vacíos ("No products match…", "Your cart is empty") manejados; nada de pantalla en blancoM2/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 el useEffect + 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 con useEffect—.
  • ui-systems-and-design-implementation — El sistema visual: Tailwind, design systems, tokens y componentes. Las className que 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: memo y 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.