Módulo 1: Thinking In Components

Por qué descomponer: reuso, aislar, razonar

Descripción

Has aprendido cómo se hace todo: qué es un componente, cómo la UI sale de las props, cómo partir una pantalla, cómo componer, cómo bajan los datos. Esta lección responde la pregunta que da sentido a todo lo anterior: ¿por qué? ¿Por qué tomarse el trabajo de partir una interfaz en piezas, en vez de escribir todo de corrido?

La respuesta son tres razones, y las tres tienen consecuencias concretas que vamos a medir, no solo afirmar.

Reuso. Defines un componente una vez y lo usas en muchos lugares. ProductCard no sirve solo para la lista: sirve también para la tira de recomendados, para el producto destacado de la portada, para los resultados de búsqueda. Una definición, muchos usos. Si no lo hubieras separado, tendrías que reescribir la tarjeta en cada lugar.

Aislamiento. Cada componente vive en su propia función, así que un cambio en uno no puede romper otro. Si tocas CartItem, ProductCard ni se entera —son piezas separadas, sin nada compartido que se pueda corromper—. El daño de un error queda contenido en su componente.

Razonar. Puedes entender, probar y arreglar una pieza pequeña a la vez, sin cargar en la cabeza la pantalla entera. ProductCard son cinco líneas que se leen solas; "el storefront completo" son cientos que no.

Vamos a ver las tres ejecutadas: el mismo ProductCard reusado en dos lugares distintos, contando cuántas veces se usó una función definida una sola vez; y el argumento del aislamiento y de razonar, aterrizado en el código.

Conexión con el módulo. Esta es la lección que amarra el módulo entero. Todo lo que construiste —descomponer (L4), componer (L5), pasar props (L6)— existe para obtener estas tres ventajas. Sin reuso, aislamiento y capacidad de razonar, partir la UI en componentes sería trabajo extra sin recompensa. Con ellas, es lo que hace posible construir y mantener una app real. Y es la antesala directa del capstone (lección 8): cuando armes el storefront completo, estarás cosechando exactamente estas tres ventajas —reusarás ProductCard y CartItem, cada pieza estará aislada, y podrás construir la app pieza por pieza sin ahogarte—.

Una analogía: los ladrillos estandarizados

Piensa en por qué la construcción moderna usa ladrillos estandarizados en vez de tallar cada piedra a medida, como se hacía hace siglos.

Reuso: el mismo ladrillo sirve para el muro de la cocina, la pared del jardín y la chimenea. Se fabrica uno (un molde) y se produce en masa. No talla nadie una piedra distinta para cada hueco. Un ProductCard es ese ladrillo: lo defines una vez y lo pones donde haga falta.

Aislamiento: si un ladrillo sale defectuoso, afecta a ese ladrillo, no a los otros diez mil del edificio. Los ladrillos no comparten una "sustancia común" que se pueda contaminar; cada uno es independiente. Si CartItem tiene un defecto, el defecto está en CartItem, no se filtra a ProductCard.

Razonar: un albañil entiende "un ladrillo" —su tamaño, su peso, cómo se coloca— y con ese conocimiento simple construye cosas enormes. No necesita entender "el edificio entero" como una masa única e irrepetible; entiende la pieza y las reglas de cómo se combinan. Tú entiendes ProductCard —cinco líneas— y con esa pieza simple, compuesta, construyes la tienda.

Contrasta con el modelo viejo: una catedral tallada piedra por piedra, cada una única, era una obra magnífica pero imposible de modificar, reproducir o reparar rápido —cada piedra era un problema nuevo—. Una UI escrita "de corrido", sin componentes, es esa catedral: impresionante quizás, pero un infierno para cambiar. Los componentes son la estandarización que cambió la construcción, aplicada a la interfaz. Guarda los ladrillos; son las tres razones en una imagen.

Ejemplo trabajado: un ProductCard, muchos usos

Vamos a medir el reuso. El mismo ProductCard, definido una sola vez, va a aparecer en dos lugares distintos de la pantalla: la lista principal (ProductList) y una tira de recomendados (Recommended). Contaremos cuántas veces se usó esa única definición.

Los componentes en JSX (la API real). Fíjate en que ProductList y Recommended son componentes distintos, con propósitos distintos, pero ambos usan el mismo ProductCard:

function ProductCard(props) {
  return (
    <article className="product-card">
      <h3 className="product-name">{props.name}</h3>
      <p className="product-price">{formatPrice(props.priceCents)}</p>
    </article>
  );
}

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

function Recommended(props) {
  return (
    <aside className="recommended">
      <h2>Recommended for you</h2>
      {props.picks.map((p) => <ProductCard key={p.id} name={p.name} priceCents={p.priceCents} />)}
    </aside>
  );
}

La versión ejecutable en Node. Ponemos un contador dentro de ProductCard para medir cuántas veces se usa la única definición:

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);
}

let productCardCalls = 0;

// UN solo ProductCard, definido una vez.
function ProductCard(props) {
  productCardCalls++;
  return h('article', { className: 'product-card' },
    h('h3', { className: 'product-name' }, props.name),
    h('p', { className: 'product-price' }, formatPrice(props.priceCents))
  );
}
function ProductList(props) {
  return h('section', { className: 'product-list' },
    props.products.map((product) => ProductCard(product))
  );
}
// REUSO: el mismo ProductCard tambien aparece en la tira de recomendados.
function Recommended(props) {
  return h('aside', { className: 'recommended' },
    h('h2', {}, 'Recommended for you'),
    props.picks.map((product) => ProductCard(product))
  );
}

const catalog = [
  { id: 'p1', name: 'Wireless Mouse',      priceCents: 2599, inStock: true },
  { id: 'p2', name: 'Mechanical Keyboard', priceCents: 8900, inStock: false },
];
const picks = [
  { id: 'p3', name: 'USB-C Hub', priceCents: 3499, inStock: true },
];

console.log('--- ProductList (usa ProductCard) ---');
console.log(renderToString(ProductList({ products: catalog })));
console.log('\n--- Recommended (REUSA el MISMO ProductCard) ---');
console.log(renderToString(Recommended({ picks })));
console.log(`\nProductCard se DEFINIO 1 vez y se USO ${productCardCalls} veces.`);

Qué esperar. Al correr el archivo, la salida es exactamente esta:

--- ProductList (usa ProductCard) ---
<section class="product-list">
  <article class="product-card">
    <h3 class="product-name">Wireless Mouse</h3>
    <p class="product-price">$25.99</p>
  </article>
  <article class="product-card">
    <h3 class="product-name">Mechanical Keyboard</h3>
    <p class="product-price">$89.00</p>
  </article>
</section>

--- Recommended (REUSA el MISMO ProductCard) ---
<aside class="recommended">
  <h2>Recommended for you</h2>
  <article class="product-card">
    <h3 class="product-name">USB-C Hub</h3>
    <p class="product-price">$34.99</p>
  </article>
</aside>
ProductCard se DEFINIO 1 vez y se USO 3 veces.

La última línea es el reuso, medido: ProductCard se definió 1 vez y se usó 3 veces. Dos veces en ProductList (mouse y teclado) y una vez en Recommended (el hub). Escribimos la tarjeta una sola vez —cinco líneas— y con ella pintamos tres tarjetas, en dos secciones con propósitos distintos. Si añadieras el producto destacado de la portada, los resultados de búsqueda, o el "también te puede interesar", cada uno reusaría el mismo ProductCard: una definición, decenas de usos.

Ahora imagina el mundo sin reuso: la tarjeta escrita a mano dentro de ProductList, y otra vez a mano dentro de Recommended, y otra vez en cada lugar. No solo es más código: es más código que hay que mantener sincronizado. El día que Mercado decida que las tarjetas muestren también la categoría, con reuso cambias ProductCard en un lugar y todas las tarjetas de la app se actualizan a la vez. Sin reuso, tendrías que encontrar y editar cada copia, y bastaría olvidar una para que la app quedara inconsistente. El reuso no es solo ahorro de tipeo; es una única fuente de verdad para cómo se ve una tarjeta.

Aislamiento: por qué tocar CartItem no rompe ProductCard

El aislamiento es la segunda razón, y se ve en la estructura misma del código. ProductCard y CartItem son dos funciones separadas. No comparten variables, no se llaman entre sí, no dependen una de la otra. Cuando modificas CartItem —digamos, para que muestre el precio de la línea además de la cantidad—, editas solo la función CartItem. ProductCard es un archivo, o un bloque, que ni tocaste. Es literalmente imposible que tu cambio en CartItem altere el comportamiento de ProductCard, porque no hay ningún punto de contacto entre los dos.

Compáralo con el componente gigante del módulo 4, donde la tarjeta, la lista y el carrito viven entrelazados en una sola función de doscientas líneas. Ahí, un cambio para arreglar el carrito puede romper la lista sin querer, porque comparten variables, estructura, y el mismo return enredado. El aislamiento que dan los componentes convierte "cambiar algo puede romper cualquier cosa" en "cambiar CartItem solo puede afectar a CartItem". Esa contención es lo que te permite modificar una app grande sin miedo.

Razonar: una pieza pequeña a la vez

La tercera razón es la más humana. Tu cabeza tiene un límite: no puedes sostener doscientas líneas entrelazadas y razonar sobre ellas con confianza. Pero sí puedes sostener ProductCard —cinco líneas, una entrada clara (las props del producto), una salida clara (la tarjeta)— y entenderlo completo, sin dudas. Cuando cada pieza es pequeña y autocontenida, razonas sobre la app una pieza a la vez: entiendes ProductCard, luego ProductList (que solo compone ProductCard), luego App (que solo compone las secciones). Nunca tienes que entender "todo junto"; entiendes cada nivel por separado y confías en los de abajo.

Esto tiene un efecto compuesto enorme. Depurar es más fácil (el bug está en una pieza, la encuentras aislada). Probar es más fácil (pruebas ProductCard con datos de mentira, sin levantar la app). Y trabajar en equipo es posible (tú tocas Cart, un compañero toca SearchBar, y no se pisan). Poder razonar sobre una pieza a la vez es lo que hace que una app crezca sin volverse inmanejable.

Errores comunes

No reusar: reescribir la misma pieza en cada lugar. Qué pasa: en vez de reusar ProductCard, se copia su marcado en la lista, en los recomendados y en la portada. Por qué pasa: copiar y pegar es rápido en el momento. Cómo detectarlo: el mismo marcado aparece en varios lugares; un cambio de diseño te obliga a editarlo en todos; alguna copia se queda desactualizada y la app se ve inconsistente. Cómo corregirlo: extrae la pieza a un componente una vez y reúsalo. La señal de que necesitas un componente es ver el mismo marcado dos veces. El reuso te da una única fuente de verdad; la copia te da un problema de sincronización que crece con el tiempo.

Descomponer sin obtener ninguna de las tres ventajas. Qué pasa: se crean componentes por crear —Wrapper1, Container2, DivHolder— que no se reúsan, no aíslan nada útil, ni ayudan a razonar. Por qué pasa: se aplica "descompón" como dogma, sin preguntar para qué. Cómo detectarlo: tienes componentes que se usan una sola vez, no tienen una responsabilidad clara, y solo agregan indirección (saltas entre archivos sin ganar claridad). Cómo corregirlo: descompón cuando obtengas al menos una de las tres ventajas —reuso (se usa en varios lados), aislamiento (separa una responsabilidad que conviene contener), o razonar (una pieza demasiado grande se vuelve entendible al partirla)—. Si un "componente" no te da ninguna, probablemente no debería existir; es solo ruido. Descomponer es un medio para estas tres cosas, no un fin en sí mismo.

Compartir estado mutable entre componentes y perder el aislamiento. Qué pasa: dos componentes leen y escriben una misma variable global, creyendo que "así se comunican". Por qué pasa: parece práctico tener un lugar común. Cómo detectarlo: un cambio en un componente afecta el comportamiento de otro sin que haya props de por medio; el aislamiento se rompe y vuelven los bugs "a distancia". Cómo corregirlo: mantén cada componente dependiendo solo de sus props (que bajan, lección 6). Si dos componentes necesitan compartir un dato, ese dato debe vivir en un ancestro común y bajar por props a ambos —eso es levantar el estado, tema de la guía más adelante (módulo 7)—, no en una variable global que los dos manosean. Compartir estado mutable global reintroduce exactamente el acoplamiento que el aislamiento venía a eliminar.

Ejercicios

Ejercicio 1 — Cuenta el reuso. Mercado quiere mostrar ProductCard en cuatro lugares: la lista principal (10 productos), los recomendados (3), el producto destacado de la portada (1), y los resultados de una búsqueda (5). ¿Cuántas veces se define ProductCard y cuántas se usa? ¿Qué pasaría si el diseñador pide agregar la categoría a la tarjeta?

Ver solución

ProductCard se define 1 vez y se usa 19 veces (10 + 3 + 1 + 5). Una sola función, diecinueve tarjetas en pantalla, repartidas en cuatro secciones distintas.

Si el diseñador pide agregar la categoría, con reuso editas ProductCard en un solo lugar —le agregas, digamos, un <span className="product-category">{props.category}</span>— y las diecinueve tarjetas, en las cuatro secciones, muestran la categoría de inmediato. Un cambio, efecto en toda la app.

Sin reuso —con el marcado de la tarjeta copiado en cada sección— tendrías que encontrar y editar el marcado en los cuatro lugares, y bastaría olvidar uno (o hacerlo distinto por error) para que la app quedara inconsistente: los recomendados mostrarían categoría y la portada no. Esa es la diferencia entre una única fuente de verdad y cuatro copias que hay que mantener sincronizadas a mano. El reuso convierte "editar en cuatro lugares y rezar" en "editar en uno".

Ejercicio 2 — Argumenta el aislamiento. Un compañero va a modificar CartItem para que muestre el subtotal de la línea (precio × cantidad). Está nervioso: "¿y si rompo la lista de productos?". Explícale, con la estructura del código, por qué su cambio en CartItem no puede afectar a ProductCard ni a ProductList.

Ver solución

No puede afectarlos porque CartItem, ProductCard y ProductList son funciones separadas, sin ningún punto de contacto. Su compañero va a editar solo el cuerpo de CartItem: agregar el cálculo del subtotal y un elemento para mostrarlo. Ese cambio vive dentro de esa función. ProductCard y ProductList son otras funciones, que ni se llaman desde CartItem ni comparten variables con él. Para que el cambio en CartItem rompiera ProductCard, tendría que haber algo compartido entre los dos que se pudiera alterar —una variable global, una dependencia directa—, y no lo hay: cada uno depende solo de sus propias props.

Esa es la garantía del aislamiento: el radio de impacto de un cambio está limitado por las fronteras del componente. Modificar CartItem solo puede afectar a CartItem (y a lo que CartItem produce en pantalla). Tu compañero puede editar con tranquilidad, probar CartItem aislado con un item de mentira, y saber que la lista de productos sigue exactamente igual. Contrasta esto con un componente gigante donde todo vive entrelazado: ahí sí, cambiar el subtotal del carrito podría romper la lista sin querer, porque compartirían el mismo enredo. El aislamiento convierte ese miedo en confianza.

Ejercicio 3 — ¿Vale la pena este componente? Para cada caso, decide si conviene crear el componente y di cuál de las tres razones (reuso, aislamiento, razonar) lo justifica —o por qué no vale la pena—: (a) un PriceTag que solo formatea y muestra un precio, usado en la tarjeta, en el carrito y en el detalle del producto; (b) un DivWrapper que solo envuelve a sus hijos en un <div>, usado una vez; (c) un Cart de sesenta líneas que maneja el carrito entero, usado una vez.

Ver solución
  • (a) PriceTag — sí conviene, por reuso. Se usa en tres lugares distintos (tarjeta, carrito, detalle), así que centraliza cómo se muestra un precio en una única fuente de verdad. Si mañana el formato cambia (agregar la moneda, tachar el precio en oferta), lo tocas en un lugar y los tres se actualizan. El reuso lo justifica de sobra.
  • (b) DivWrapper — no conviene. Se usa una sola vez, no aísla ninguna responsabilidad significativa (solo pone un <div>), y no ayuda a razonar (agrega un salto de indirección sin claridad a cambio). No obtiene ninguna de las tres ventajas; es ruido. Deja ese <div> inline donde se necesita.
  • (c) Cart — sí conviene, por aislamiento y por razonar. Aunque se use una sola vez, sesenta líneas que manejan "el carrito" son una responsabilidad grande y bien definida. Sacarla a su propio componente aísla esa complejidad (un bug del carrito queda contenido en Cart) y te permite razonar sobre "el carrito" como una pieza, sin mezclarlo con el resto de App. El reuso no es la única razón válida: aislar una responsabilidad grande y poder pensarla por separado justifica un componente aunque se use una vez.

La regla que sale de los tres: crea un componente cuando obtengas al menos una de las tres ventajas. PriceTag gana reuso; Cart gana aislamiento y claridad; DivWrapper no gana nada, así que no debería existir.

Resumen y siguiente paso

En esta lección respondiste la pregunta que da sentido a todo el módulo: ¿por qué descomponer? Por tres razones, y las mediste. Reuso: ejecutaste un ProductCard definido una vez y usado tres veces en dos secciones distintas —una única fuente de verdad para cómo se ve una tarjeta—. Aislamiento: viste que ProductCard y CartItem son funciones separadas sin punto de contacto, así que un cambio en una no puede romper la otra —el radio de impacto queda contenido—. Razonar: entendiste que puedes sostener y probar una pieza pequeña a la vez, en lugar de cientos de líneas entrelazadas. Y la regla que las une: descompón cuando obtengas al menos una de las tres; si un componente no da ninguna, es ruido.

Antes de avanzar deberías poder: explicar las tres razones con los ladrillos estandarizados; medir el reuso de un componente y decir qué cambia si se edita una vez; argumentar por qué el aislamiento contiene el daño de un cambio; y decidir si un componente propuesto vale la pena según las tres ventajas.

La lección 8 es el capstone del módulo: vas a armar el storefront estático de Mercado de punta a punta, cosechando estas tres ventajas a la vez. Ensamblarás AppSearchBar + ProductListProductCard; AppCartCartItem, con su árbol de componentes, su código React real, y su lógica ejecutada en Node —App(data) renderizado a string, con datos fijos, produciendo el HTML de toda la tienda—. Todo lo que aprendiste, junto y corriendo.

Recursos