Módulo 1: Thinking In Components

Mini-proyecto: arma el storefront estático de Mercado

Descripción

Es hora de que juntes todo. A lo largo del módulo construiste las piezas por separado —ProductCard, SearchBar, CartItem— y aprendiste los conceptos que las unen —componer, pasar props hacia abajo, descomponer con criterio—. En este mini-proyecto ensamblas el storefront estático de Mercado de punta a punta: la tienda completa, con su barra de búsqueda, su lista de productos y su carrito, todo a partir de unos datos fijos.

Es la síntesis del módulo, y el ensayo de lo que harás a mayor escala en el capstone de la guía (módulo 8), cuando la tienda tenga estado, eventos y efectos. Aquí todo es estático —UI = f(props)—, pero la estructura es la real: el mismo árbol de componentes, el mismo flujo de datos, los mismos nombres.

Tu entrega tiene tres partes:

  1. El árbol de componentes. El diagrama de cómo se anidan App, SearchBar, ProductList, ProductCard, Cart y CartItem —la jerarquía completa del storefront—.
  2. El código React real. Cada componente escrito en JSX (la API que escribirías en un proyecto de verdad), y su equivalente ejecutable en Node.
  3. La lógica ejecutada. App(data) renderizado a string, con datos fijos, produciendo el HTML de toda la tienda —la salida literal—.

Conexión con el módulo. Este proyecto cierra el módulo 1 juntando sus seis lecciones en un solo sistema ejecutado. Vas a cosechar, a la vez, las tres ventajas de la lección 7: reusarás ProductCard (uno por producto) y CartItem (uno por línea), cada pieza estará aislada en su función, y podrás construir la app razonando una pieza a la vez. Y prepara el terreno para lo que viene: en el módulo 2 profundizarás el JSX que aquí solo usaste; en el módulo 3 le darás estado a esta tienda (para que la búsqueda filtre de verdad, para que el carrito crezca al hacer clic); en el módulo 4, los eventos que disparan esos cambios. Lo que armas hoy es el esqueleto estático sobre el que todo eso se montará.

Una analogía: armar el mueble modular siguiendo el instructivo

Ya conoces las piezas (los módulos de la lección 4) y sabes cómo encajan (la composición de la lección 5). Armar el storefront es como sentarte con todas las piezas del mueble modular sobre el piso y seguir el instructivo para ensamblarlo: primero los sub-módulos pequeños (los cajones, las repisas), luego los módulos medianos, y al final el mueble completo. No inventas piezas nuevas; ensamblas las que ya tienes, en el orden correcto, guiado por el plano (el árbol de componentes).

El instructivo tiene una lógica de abajo hacia arriba: no puedes poner el mueble en la pared antes de armar los cajones. Igual aquí: App se apoya en ProductList, que se apoya en ProductCard; para que App funcione, las piezas de abajo tienen que estar listas. Por eso construiremos el código de las hojas (ProductCard, CartItem) hacia la raíz (App). Y como en el mueble, la recompensa es el momento de ensamblar: cada pieza que ya probaste sola encaja con las demás y, de golpe, tienes la tienda entera. Guarda el instructivo; es el plan de este proyecto.

La solución de referencia, ejecutada

Vamos a construir la solución completa y correrla, para que tengas un patrón claro. (Los ejercicios del final te piden extenderla y razonar sobre ella.)

Parte 1 — El árbol de componentes

El storefront completo, como jerarquía de componentes:

flowchart TD
    App[App] --> SearchBar[SearchBar]
    App --> ProductList[ProductList]
    App --> Cart[Cart]
    ProductList --> PC1["ProductCard (p1)"]
    ProductList --> PC2["ProductCard (p2)"]
    ProductList --> PC3["ProductCard (p3)"]
    Cart --> CI1["CartItem (p1)"]
    Cart --> CI2["CartItem (p3)"]

Léelo de arriba hacia abajo: App es la raíz y contiene tres hijos —SearchBar, ProductList, Cart—. ProductList compone un ProductCard por producto (aquí tres). Cart compone un CartItem por línea del carrito (aquí dos). Es exactamente el árbol que dibujaste en la lección 4, ahora completo y listo para ensamblar.

Y los datos que van a alimentarlo, fijos:

query:      "usb"                              (lo que hay escrito en la busqueda)
products:   [ Wireless Mouse   $25.99  in stock,
              Mechanical Keyboard $89.00 out of stock,
              USB-C Hub        $34.99  in stock ]
cartItems:  [ Wireless Mouse  x1,
              USB-C Hub       x2 ]

Parte 2 — El código React real (JSX)

Así se ven los seis componentes en JSX, la API que escribirías en un proyecto real. Van de la hoja a la raíz, como el instructivo del mueble:

function SearchBar(props) {
  return (
    <form className="search-bar">
      <input className="search-input" value={props.query} placeholder="Search products" />
    </form>
  );
}

function ProductCard(props) {
  return (
    <article className="product-card">
      <h3 className="product-name">{props.name}</h3>
      <p className="product-price">{formatPrice(props.priceCents)}</p>
      <span className="product-stock">{props.inStock ? 'In stock' : 'Out of stock'}</span>
    </article>
  );
}

function ProductList(props) {
  return (
    <section className="product-list">
      {props.products.map((product) => (
        <ProductCard key={product.id} name={product.name}
                     priceCents={product.priceCents} inStock={product.inStock} />
      ))}
    </section>
  );
}

function CartItem(props) {
  return (
    <li className="cart-item">
      <span className="cart-item-name">{props.name}</span>
      <span className="cart-item-qty">{'x' + props.quantity}</span>
    </li>
  );
}

function Cart(props) {
  return (
    <aside className="cart">
      <h2>Your cart</h2>
      <ul className="cart-items">
        {props.items.map((item) => (
          <CartItem key={item.id} name={item.name} quantity={item.quantity} />
        ))}
      </ul>
    </aside>
  );
}

function App(props) {
  return (
    <div className="storefront">
      <SearchBar query={props.query} />
      <ProductList products={props.products} />
      <Cart items={props.cartItems} />
    </div>
  );
}

Observa a App: es puro ensamblaje. No dibuja nada por su cuenta; solo compone sus tres hijos y les baja los datos que cada uno necesita —query a SearchBar, products a ProductList, cartItems a Cart (como items)—. Cada hijo, a su vez, reparte hacia abajo lo suyo. Es el flujo unidireccional de la lección 6, aplicado a la tienda entera.

Parte 3 — La lógica ejecutada en Node

Y aquí está todo junto, ejecutable, con los datos fijos. Reunimos los seis componentes, el renderToString (con soporte para el <input> void), y renderizamos App(data):

function h(tag, props, ...children) {
  return { tag, props: props || {}, children: children.flat() };
}
const VOID = new Set(['input', 'img', 'br', 'hr']);
function renderToString(node, indent = 0) {
  const pad = '  '.repeat(indent);
  if (node == null || typeof node === 'boolean') return '';
  if (typeof node !== 'object') return pad + node;
  const attrs = Object.entries(node.props)
    .map(([k, v]) => ` ${k === 'className' ? 'class' : k}="${v}"`).join('');
  if (VOID.has(node.tag)) return `${pad}<${node.tag}${attrs} />`;
  const kids = node.children.filter((c) => c != null && c !== false);
  if (kids.length <= 1 && kids.every((c) => typeof c !== 'object'))
    return `${pad}<${node.tag}${attrs}>${kids[0] ?? ''}</${node.tag}>`;
  const inner = kids.map((c) => renderToString(c, indent + 1)).join('\n');
  return `${pad}<${node.tag}${attrs}>\n${inner}\n${pad}</${node.tag}>`;
}
function formatPrice(cents) {
  return '$' + (cents / 100).toFixed(2);
}

function SearchBar(props) {
  return h('form', { className: 'search-bar' },
    h('input', { className: 'search-input', value: props.query, placeholder: 'Search products' })
  );
}
function ProductCard(props) {
  return h('article', { className: 'product-card' },
    h('h3', { className: 'product-name' }, props.name),
    h('p', { className: 'product-price' }, formatPrice(props.priceCents)),
    h('span', { className: 'product-stock' },
      props.inStock ? 'In stock' : 'Out of stock')
  );
}
function ProductList(props) {
  return h('section', { className: 'product-list' },
    props.products.map((product) => ProductCard(product))
  );
}
function CartItem(props) {
  return h('li', { className: 'cart-item' },
    h('span', { className: 'cart-item-name' }, props.name),
    h('span', { className: 'cart-item-qty' }, 'x' + props.quantity)
  );
}
function Cart(props) {
  return h('aside', { className: 'cart' },
    h('h2', {}, 'Your cart'),
    h('ul', { className: 'cart-items' },
      props.items.map((item) => CartItem(item))
    )
  );
}
function App(props) {
  return h('div', { className: 'storefront' },
    SearchBar({ query: props.query }),
    ProductList({ products: props.products }),
    Cart({ items: props.cartItems })
  );
}

const products = [
  { id: 'p1', name: 'Wireless Mouse',      priceCents: 2599, category: 'accessories', inStock: true },
  { id: 'p2', name: 'Mechanical Keyboard', priceCents: 8900, category: 'accessories', inStock: false },
  { id: 'p3', name: 'USB-C Hub',           priceCents: 3499, category: 'accessories', inStock: true },
];
const cartItems = [
  { id: 'p1', name: 'Wireless Mouse', quantity: 1 },
  { id: 'p3', name: 'USB-C Hub',      quantity: 2 },
];

const data = { query: 'usb', products, cartItems };
console.log(renderToString(App(data)));

Qué esperar. Al correr el archivo, la salida es exactamente esta —el HTML de toda la tienda—:

<div class="storefront">
  <form class="search-bar">
    <input class="search-input" value="usb" placeholder="Search products" />
  </form>
  <section class="product-list">
    <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>
    </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>
    </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>
    </article>
  </section>
  <aside class="cart">
    <h2>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>
      </li>
      <li class="cart-item">
        <span class="cart-item-name">USB-C Hub</span>
        <span class="cart-item-qty">x2</span>
      </li>
    </ul>
  </aside>
</div>

Esta salida es el módulo entero, en un solo HTML. Recórrelo y reconoce cada concepto que aprendiste:

  • El <div class="storefront"> que envuelve todo lo produjo App, que solo ensambló sus tres hijos (composición, lección 5).
  • El <input value="usb" /> muestra el query que App bajó a SearchBar (props hacia abajo, lección 6). Y salió como elemento void, sin cierre (la ampliación de la lección 4).
  • Los tres <article> los produjo ProductCard, uno por producto, invocado por ProductList (reuso, lección 7). Cada uno muestra datos distintos porque recibió props distintas (UI = f(props), lección 3): fíjate en el teclado, que dice "Out of stock" porque su inStock era false.
  • Los precios aparecen formateados —$25.99, $89.00, $34.99— porque ProductCard transformó los centavos con formatPrice.
  • Los dos <li> del carrito los produjo CartItem, uno por línea, invocado por Cart (composición y reuso otra vez, con una pieza distinta).
  • Y todo el anidamiento del HTML refleja, nivel por nivel, el árbol de componentes de la Parte 1.

Nada de esto se manipuló a mano en ninguna pantalla. Escribiste seis funciones que describen cómo se ve cada pieza dados sus datos, las compusiste en App, y un renderizador las convirtió en HTML. Eso es pensar en componentes: la tesis del módulo, entregada.

Errores comunes

Ensamblar sin bajar los datos correctos. Qué pasa: se compone App con sus tres hijos, pero se olvida pasarles las props —o se les pasa la prop con el nombre equivocado—, y los hijos reciben undefined. Por qué pasa: al ensamblar rápido, es fácil escribir <ProductList /> sin el products={...}. Cómo detectarlo: un componente hijo produce HTML vacío o revienta al hacer .map de undefined; en el árbol, una rama entera "no aparece". Cómo corregirlo: cada vez que compones un hijo, verifica que le bajas todas las props que necesita, con el nombre que ese hijo espera. App es responsable de darle a ProductList su products, a SearchBar su query, a Cart sus items. El ensamblaje no es solo poner los hijos en su lugar; es conectar el flujo de datos desde la raíz hasta cada hoja.

Meter lógica pesada dentro del ensamblaje. Qué pasa: dentro de App (o de ProductList) alguien empieza a filtrar, ordenar o calcular cosas complejas en medio del JSX. Por qué pasa: como los datos están ahí, tienta procesarlos en el momento. Cómo detectarlo: App deja de ser un ensamblador simple y se llena de lógica; cuesta ver "qué compone" entre tanto cálculo. Cómo corregirlo: en este módulo, App solo ensambla y baja datos; los datos llegan ya listos. Cuando necesites derivar cosas de los datos —filtrar la lista por el query, ordenar por precio, sumar el total del carrito— eso es estado derivado, y tiene su lugar propio (módulo 5), computado con cuidado, no incrustado en el ensamblaje. Mantén App como lo que es aquí: un componente que compone, no que calcula.

Renderizar una lista sin datos estables por elemento. Qué pasa: al componer ProductList o Cart, se usan datos sin un id estable, o se omite la key en React. Por qué pasa: con todo estático "funciona igual", así que no se nota la falta. Cómo detectarlo: React advierte sobre la key faltante; y cuando el módulo 3 agregue estado (reordenar, filtrar), aparecen bugs de identidad. Cómo corregirlo: cada elemento de una lista necesita una key única y estable —el id del producto o del item—, no su posición. Aquí lo preparamos usando id en cada dato desde el principio. Acostumbrarte a ello ahora, con todo estático, te evita el dolor cuando el módulo 2 (listas y key) y el módulo 3 (estado) lleguen. La identidad estable de cada elemento es una inversión que se cobra después.

Ejercicios

Ejercicio 1 — Agrega el total del carrito. Extiende Cart para que, además de las líneas, muestre el total en un <p className="cart-total">. El total es la suma de priceCents × quantity de cada línea, formateado con formatPrice. Necesitarás que cada item del carrito incluya su priceCents. Escribe el Cart extendido y calcula, a mano, cuánto daría el total para el carrito de la solución de referencia (Wireless Mouse $25.99 x1, USB-C Hub $34.99 x2).

Ver solución

Primero, los items necesitan priceCents. Con items = [{ id: 'p1', name: 'Wireless Mouse', priceCents: 2599, quantity: 1 }, { id: 'p3', name: 'USB-C Hub', priceCents: 3499, quantity: 2 }], el Cart extendido:

function Cart(props) {
  const totalCents = props.items.reduce(
    (sum, item) => sum + item.priceCents * item.quantity, 0
  );
  return h('aside', { className: 'cart' },
    h('h2', {}, 'Your cart'),
    h('ul', { className: 'cart-items' },
      props.items.map((item) => CartItem(item))
    ),
    h('p', { className: 'cart-total' }, 'Total: ' + formatPrice(totalCents))
  );
}

El cálculo a mano: 2599 × 1 = 2599; 3499 × 2 = 6998; suma = 9597 centavos; formatPrice(9597) = $95.97. Así que el <p class="cart-total"> mostraría Total: $95.97.

Nota dos cosas. Una: calculamos el total en centavos (enteros) y solo formateamos al final —nunca sumamos dólares con decimales, para no arrastrar errores de flotante—. Dos: totalCents es un valor derivado de las props (props.items), calculado en el render, no un dato guardado aparte. Eso es un adelanto del "derivar, no guardar" del módulo 5: el total no se almacena; se computa de los datos cada vez.

Ejercicio 2 — Un carrito vacío. ¿Qué HTML produce Cart (el de la solución de referencia, sin total) si props.items es una lista vacía []? Escríbelo. ¿Rompe algo? Explica por qué el componente maneja bien ese caso sin código especial.

Ver solución

Con props.items = [], el map no produce ningún CartItem, así que el <ul> queda sin hijos:

<aside class="cart">
  <h2>Your cart</h2>
  <ul class="cart-items"></ul>
</aside>

No rompe nada. El <h2>Your cart</h2> sigue apareciendo (es marcado fijo de Cart), y el <ul> sale vacío porque [].map(...) devuelve una lista vacía —cero elementos, cero <li>—. renderToString lo maneja solo: un <ul> sin hijos se escribe como <ul class="cart-items"></ul>.

El componente maneja bien el caso vacío sin código especial porque está escrito como UI = f(props): describe "por cada item, un CartItem", y si no hay items, no hay CartItem, punto. No necesita un if (items.length === 0) para el caso vacío; la descripción declarativa ya lo cubre. Esa es una ventaja del modelo: la misma función correcta funciona para cero, dos o mil items, porque no asume una cantidad. (Si quisieras mostrar un mensaje "tu carrito está vacío", ahí sí agregarías una condición —renderizado condicional, módulo 2—, pero es una decisión de diseño, no una necesidad para que no reviente.)

Ejercicio 3 — Reusa ProductCard en una sección nueva. Mercado quiere agregar, dentro de App, una sección de "Featured" que muestre un producto destacado usando el mismo ProductCard. Escribe el componente Featured (recibe un producto como prop product) y muestra cómo lo compondrías dentro de App. Explica cuál de las tres razones de la lección 7 estás aprovechando.

Ver solución
function Featured(props) {
  return h('section', { className: 'featured' },
    h('h2', {}, 'Featured'),
    ProductCard(props.product)
  );
}

function App(props) {
  return h('div', { className: 'storefront' },
    SearchBar({ query: props.query }),
    Featured({ product: props.featured }),
    ProductList({ products: props.products }),
    Cart({ items: props.cartItems })
  );
}

Featured recibe un producto en props.product y lo muestra componiendo el mismo ProductCard que ya usa ProductList —sin reescribir nada de la tarjeta—. Dentro de App, lo compones como un hijo más y le bajas el producto destacado (props.featured).

La razón que aprovechas es el reuso (lección 7): ProductCard se definió una vez y ahora aparece también en Featured, además de en la lista. Si mañana cambias cómo se ve una tarjeta, Featured se actualiza igual que la lista, gratis, porque las dos usan la misma pieza. Escribiste una sección nueva sin escribir una tarjeta nueva: exactamente el pago del trabajo de haber separado ProductCard desde el principio.

Resumen y siguiente paso

En este mini-proyecto armaste el storefront estático de Mercado de punta a punta, juntando las seis lecciones del módulo en un solo sistema ejecutado. Entregaste las tres partes: el árbol de componentes completo (AppSearchBar + ProductListProductCard; AppCartCartItem), el código React real de cada componente en JSX, y la lógica ejecutadaApp(data) renderizado a string, produciendo el HTML de toda la tienda con datos fijos—. En esa salida reconociste, junto y corriendo, todo lo del módulo: la composición que ensambla, las props que bajan, el reuso de ProductCard y CartItem, y UI = f(props) haciendo que cada pieza muestre sus propios datos. Y viste que App es puro ensamblaje: compone y baja datos, no calcula ni manipula pantallas.

Con esto cierras el módulo 1. Ahora puedes mirar una interfaz y pensarla en componentes: descomponerla en un árbol, escribir cada pieza como una función pura de sus props, componerlas, y hacer que los datos fluyan hacia abajo. Ese es el modelo mental de React; el resto de la guía lo llena de vida.

Hacia dónde sigues. El módulo 2 toma el JSX que aquí solo usaste y lo profundiza: las expresiones {}, los atributos, el renderizado condicional, las listas y por qué necesitan key. El módulo 3 le da estado a esta tienda —lo que hace que la UI cambie sola: la búsqueda que filtra al escribir, el carrito que crece al hacer clic—, pasando de UI = f(props) a UI = f(estado). El módulo 4 trae los eventos que disparan esos cambios (onClick, onChange) y los callbacks que comunican hacia arriba. Tu storefront estático es el esqueleto; en los próximos módulos aprende a moverse.

Recursos

  • React, "Thinking in React" — react.dev/learn/thinking-in-react. El recorrido oficial de descomponer una UI, construir una versión estática con props, y luego (en módulos siguientes) agregarle estado. Es, casi paso a paso, lo que hiciste en este módulo. En inglés.
  • React, "Your First Component" — react.dev/learn/your-first-component. Repaso de la definición de componente y su organización, útil al ensamblar varios. En inglés.
  • React, "Passing Props to a Component" — react.dev/learn/passing-props-to-a-component. Cómo conectar el flujo de datos de la raíz a las hojas al ensamblar la app. En inglés.
  • React, "Describing the UI" — react.dev/learn/describing-the-ui. El índice de la sección que cubre todo lo de este módulo; buen punto para consolidar antes de pasar a "Adding Interactivity" (módulos 3 y 4). En inglés.