Módulo 2: Jsx And Rendering

Describir la UI, no manipular el DOM

Descripción

Este módulo empezó con una tesis técnica —JSX es JavaScript— y termina con la tesis filosófica que la sostiene, la misma que abrió la guía en el módulo 1: React es declarativo. Todo lo que aprendiste aquí —los huecos con expresiones, el condicional, las listas, la key— son piezas de una sola idea: no le dices a la pantalla cómo cambiar; le describes cómo debe verse para los datos actuales, y React se encarga del cómo.

La palabra clave es describir frente a manipular. En el estilo viejo (imperativo), tú das órdenes paso a paso: "busca el nodo del stock, cámbiale el texto a 'Out of stock', agrégale la clase 'sold-out', crea un <span> para el badge, insértalo dentro del `

'". Eres responsable de encontrar cada pedazo de pantalla y de mutarlo en el orden correcto, cada vez que un dato cambia. En el estilo React (declarativo), tú describes el resultado: "para un producto agotado, la tarjeta se ve así" —con su texto y su badge—, y cuando el dato cambia, describes de nuevo cómo se ve, entero. React compara tu descripción nueva con la anterior y hace los cambios mínimos en el DOM. Tú nunca tocas el DOM.

Esta lección hace explícito ese contraste, muestra cómo el condicional y las listas encajan en la postura declarativa (no "agregas" ni "quitas" nodos: describes la UI completa para cada estado), y lo ejecuta: el mismo componente re-descrito para dos estados del dato, sin tocar ninguna pantalla ni mutar la entrada.

Conexión con el módulo. Es la lección-síntesis. Recoge el condicional (lección 4), las listas (5) y la key (6) y muestra que todos son formas de describir, no de manipular. Y cierra el círculo con el módulo 1, que abrió con UI = f(datos): describir es, precisamente, escribir esa f. Prepara el módulo 3, donde el "dato cambia" dejará de ser "nosotros llamamos la función con props distintas" y pasará a ser el estado cambiando solo; pero la postura —describir, no manipular— será exactamente la misma.

Una analogía: el pedido en el restaurante vs cocinar tú

Piensa en la diferencia entre pedir un platillo y cocinarlo paso a paso.

Cuando cocinas (imperativo), tú das cada instrucción en orden: pon la sartén al fuego, echa aceite, espera que caliente, agrega la cebolla, revuelve dos minutos, agrega el arroz... Si algo cambia —quieres el platillo sin cebolla—, tienes que saber en qué paso intervenir y cómo ajustar los siguientes. Eres responsable de toda la secuencia y de cada transición.

Cuando pides en un restaurante (declarativo), tú describes el resultado que quieres: "arroz frito con pollo, sin cebolla, término medio". No dices cómo cocinarlo; describes qué quieres, y la cocina se encarga del cómo. Si cambias de opinión —"que sea sin pollo"—, no editas la receta paso a paso: das una nueva descripción del plato ("arroz frito de verduras") y la cocina lo prepara. Describes el qué; el cómo es problema de la cocina.

React es el restaurante. Tú describes cómo se ve la UI para unos datos ("para este producto agotado, la tarjeta se ve así: nombre, precio, 'Out of stock', y un badge 'Sold out'"). No manipulas la pantalla paso a paso. Y si el dato cambia, no editas el DOM a mano: das una nueva descripción —vuelves a llamar a la función con el dato nuevo— y React, la cocina, hace los cambios. Guarda la imagen: pides un resultado, no dictas una receta.

El contraste, en código

Veamos el mismo objetivo —reflejar que un producto se agotó— en los dos estilos, para que el contraste se sienta.

Estilo imperativo (el que React reemplaza). Tú buscas los nodos y los mutas, en orden:

// Imperativo: das ordenes para CAMBIAR la pantalla, paso a paso.
const stock = document.querySelector('#product-p1 .product-stock');
stock.textContent = 'Out of stock';               // cambia el texto
stock.classList.add('is-out');                    // agrega una clase

const badge = document.createElement('span');     // crea el badge
badge.className = 'badge badge-out';
badge.textContent = 'Sold out';
document.querySelector('#product-p1').appendChild(badge);  // lo inserta

const button = document.querySelector('#product-p1 button');
button.disabled = true;                           // deshabilita el boton

Cuenta cuántas cosas tienes que saber: cuáles nodos existen, en qué orden mutarlos, qué había antes (¿ya estaba el badge de un cambio anterior? habría que evitar duplicarlo), y repetir todo esto cada vez que el stock cambie. Eres responsable de las transiciones.

Estilo React (declarativo). Tú describes cómo se ve la tarjeta para este dato, entera:

// Declarativo: describes como se ve la UI para los datos actuales.
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>
      {!props.inStock && <span className="badge badge-out">Sold out</span>}
      <button disabled={!props.inStock}>Add to cart</button>
    </article>
  );
}

Aquí no hay querySelector, ni createElement, ni appendChild, ni un orden de pasos. Solo una descripción: para un producto, la tarjeta es esto. Si inStock es true, se describe con "In stock", sin badge y con el botón habilitado; si es false, con "Out of stock", con badge y con el botón deshabilitado. Tú solo razonas sobre estados ("¿cómo se ve para estos datos?"), no sobre transiciones ("¿qué había, qué debo cambiar?"). React se encarga de las transiciones —y de no duplicar el badge, y del orden— comparando tu descripción nueva con la anterior.

Ejemplo trabajado: re-describir, no mutar

Vamos a ejecutar el punto: cuando el dato cambia, no mutamos nada; volvemos a describir. Tomamos un producto que pasa de en stock a agotado, y llamamos al mismo componente con cada estado del dato. Verás dos descripciones distintas salir de la misma función, sin que nadie toque una pantalla —y sin que el dato original se altere—.

Primero, en JSX real —usando un fragmento (lección 2) para agrupar el texto de stock y el badge—:

function StockLine(props) {
  return (
    <>
      <span className="product-stock">
        {props.inStock ? 'In stock' : 'Out of stock'}
      </span>
      {!props.inStock && <span className="badge badge-out">Sold out</span>}
    </>
  );
}

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

Ahora la versión ejecutable en Node:

function h(tag, props, ...children) {
  return { tag, props: props || {}, children: children.flat() };
}
const Fragment = 'FRAGMENT';
const VOID = new Set(['input', 'img', 'br', 'hr']);
const ATTR = { className: 'class', htmlFor: 'for' };
function renderToString(node, indent = 0) {
  const pad = '  '.repeat(indent);
  if (node == null || typeof node === 'boolean') return '';
  if (typeof node !== 'object') return pad + node;
  if (node.tag === Fragment) {
    return node.children
      .filter((c) => c != null && c !== false)
      .map((c) => renderToString(c, indent)).join('\n');
  }
  const attrs = Object.entries(node.props)
    .filter(([, v]) => v !== false && v != null)
    .map(([k, v]) => (v === true ? ` ${ATTR[k] || k}` : ` ${ATTR[k] || 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 StockLine(props) {
  return h(Fragment, {},
    h('span', { className: 'product-stock' },
      props.inStock ? 'In stock' : 'Out of stock'),
    !props.inStock && h('span', { className: 'badge badge-out' }, 'Sold out')
  );
}
function ProductCard(props) {
  return h('article', { className: 'product-card' },
    h('h3', { className: 'product-name' }, props.name),
    h('p', { className: 'product-price' }, formatPrice(props.priceCents)),
    StockLine(props)
  );
}

// Un mismo producto en dos ESTADOS del dato: en stock -> agotado.
const inStock = { id: 'p1', name: 'Wireless Mouse', priceCents: 2599, inStock: true };
const soldOut = { ...inStock, inStock: false };

console.log('=== Describir, no manipular: cambia el dato, re-describe ===');
console.log('render #1  (inStock: true):');
console.log(renderToString(ProductCard(inStock)));
console.log('\nrender #2  (inStock: false) -- MISMA funcion, dato nuevo:');
console.log(renderToString(ProductCard(soldOut)));

// La funcion no busco ni modifico ninguna pantalla, y no muto el dato de entrada.
console.log('\nEl dato original NO se muto:', inStock.inStock === true);

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

=== Describir, no manipular: cambia el dato, re-describe ===
render #1  (inStock: true):
<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>

render #2  (inStock: false) -- MISMA funcion, dato nuevo:
<article class="product-card">
  <h3 class="product-name">Wireless Mouse</h3>
  <p class="product-price">$25.99</p>
  <span class="product-stock">Out of stock</span>
  <span class="badge badge-out">Sold out</span>
</article>

El dato original NO se muto: true

Lee la salida como la síntesis del módulo.

El "render #1" y el "render #2" son dos descripciones completas, de la misma función. Para reflejar que el producto se agotó, no editamos el primer render —no le buscamos el <span> del stock para cambiarle el texto, ni le insertamos el badge—. Llamamos a ProductCard otra vez, con el dato nuevo (soldOut), y obtuvimos una descripción entera y nueva: el texto pasó a "Out of stock" y apareció el badge "Sold out", todo de una. Eso es declarar: describes la UI completa para cada estado del dato, y el "cambio" es la diferencia entre dos descripciones, que React calcula por ti. Tú nunca mutaste una pantalla.

Y el dato original no se tocó: inStock.inStock === true sigue siendo true. ProductCard es una función pura —no modifica sus entradas, no toca nada de afuera; dado un dato, produce una descripción y ya—. soldOut lo creamos como una copia con { ...inStock, inStock: false }, sin alterar inStock. Esta pureza es lo que hace confiable el modelo declarativo: cada render depende solo de los datos que entran, así que puedes razonar sobre "cómo se ve para estos datos" sin preocuparte por qué pasó antes.

Profundización: por qué declarar es más simple

El estilo declarativo no es solo "más elegante": reduce de verdad la carga mental, y vale la pena nombrar por qué.

Razonas sobre estados, no sobre transiciones. En imperativo, para N datos que cambian tienes que pensar en todas las transiciones posibles ("si estaba en stock y se agota... pero ¿y si ya tenía el badge de antes?, ¿y si el botón ya estaba deshabilitado?"). El número de transiciones explota. En declarativo, solo describes cómo se ve para cada estado —un problema por estado, no por par de estados—. React se encarga de ir de un estado al otro.

No hay estado "escondido" en el DOM. En imperativo, la verdad de la UI vive en el DOM, y tú la vas mutando; es fácil que el DOM y tus datos se desincronicen (el badge quedó de un cambio anterior, el texto dice una cosa y el dato otra). En declarativo, la fuente de verdad son tus datos, y la UI siempre es su reflejo: UI = f(datos). Si los datos son correctos, la UI es correcta, porque se deriva de ellos en cada render.

El condicional y las listas son declarativos por naturaleza. Fíjate en cómo encajan las piezas del módulo. El badge con && no dice "agrega un badge"; dice "para un producto agotado, existe un badge". El .map() no dice "por cada producto nuevo, inserta una tarjeta"; dice "la lista es una tarjeta por producto". La key no es una instrucción de manipulación; es la identidad que le permite a React reconciliar dos descripciones. Todo el módulo ha sido, en realidad, aprender a describir.

Profundización: "pero yo vi document.getElementById..."

Un matiz honesto, para que no te confunda después. Que React sea declarativo no significa que el DOM imperativo desaparezca del mundo: significa que casi nunca lo tocas; React lo toca por ti. Hay un puñado de casos donde, dentro de React, sí se accede a un nodo real del DOM —dar foco a un input, medir el tamaño de un elemento, integrar una librería que no es de React—, y para eso existe un mecanismo específico (las refs), que es tema de más adelante. Pero son la excepción, no la regla, y siempre para cosas que el modelo declarativo genuinamente no cubre (el foco no es un "dato" que describas). La regla del 99% del tiempo sigue en pie: describe la UI a partir de los datos; no salgas a buscar y mutar nodos. Si te descubres queriendo querySelector dentro de un componente para cambiar lo que se ve, casi siempre la respuesta correcta es "cambia el dato y deja que React re-describa".

Errores comunes

Salir a manipular el DOM dentro de un componente. Qué pasa: dentro de un componente se usa document.getElementById(...).textContent = ... o .classList.add(...) para cambiar lo que se ve. Por qué pasa: es el reflejo imperativo de años de JavaScript. Cómo detectarlo: tu componente contiene querySelector, createElement, appendChild, innerHTML para cambiar su propia salida. Cómo corregirlo: no manipules; describe. Cambia el dato (una prop, y en el módulo 3 el estado) y deja que React re-describa la UI. Si querías mostrar el badge, no lo insertes: descríbelo con {!inStock && <badge/>}. El DOM es cosa de React, no tuya.

Mutar los datos en vez de describir con datos nuevos. Qué pasa: para reflejar un cambio se muta el dato existente (product.inStock = false; product.badges.push(...)) en el lugar. Por qué pasa: parece lo directo. Cómo detectarlo: modificas objetos o arrays que ya existen en vez de crear versiones nuevas; y —en el módulo 3— React "no re-renderiza" porque no detecta el cambio. Cómo corregirlo: trata los datos como inmutables: crea versiones nuevas ({ ...product, inStock: false }, [...list, nuevo]) y describe con ellas. Lo viste: soldOut fue una copia, y inStock no se tocó. La pureza (no mutar entradas) es lo que hace confiable el modelo declarativo; el módulo 3 lo vuelve regla dura para el estado.

Pensar en "agregar/quitar" en vez de en "cómo se ve". Qué pasa: al diseñar un componente se piensa en verbos de transición —"cuando pase X, agrego el badge; cuando pase Y, lo quito"—. Por qué pasa: es como uno describe una interacción en voz alta. Cómo detectarlo: tu diseño está lleno de "cuando... entonces cambio...", y te cuesta escribir el componente. Cómo corregirlo: reformula en términos de estados: "para un producto en stock, la tarjeta se ve así; para uno agotado, así". Describe cada estado por completo, y usa el condicional para elegir. React se encarga del "agregar/quitar" al ir de un estado al otro. Piensa en fotos (estados), no en el video (transiciones).

Ejercicios

Ejercicio 1 — Traduce de imperativo a declarativo. Este código imperativo cambia el precio mostrado cuando hay descuento. Reescríbelo como un componente React declarativo que reciba props.priceCents y props.salePriceCents (este último puede ser null si no hay oferta):

const priceEl = document.querySelector('#product .price');
if (product.salePriceCents != null) {
  priceEl.textContent = formatPrice(product.salePriceCents);
  priceEl.classList.add('on-sale');
} else {
  priceEl.textContent = formatPrice(product.priceCents);
}
Ver solución
function Price(props) {
  const onSale = props.salePriceCents != null;
  return (
    <span className={onSale ? 'price on-sale' : 'price'}>
      {formatPrice(onSale ? props.salePriceCents : props.priceCents)}
    </span>
  );
}

En vez de buscar el nodo del precio y mutarlo según la condición, describimos cómo se ve el precio para los datos: si hay oferta (salePriceCents != null), la clase incluye on-sale y el texto es el precio de oferta; si no, la clase normal y el precio normal. No hay querySelector ni classList.add: un ternario para la clase (lección 3 y 4) y otro para el texto. La condición se calcula una vez arriba (onSale) y se usa en dos expresiones. React se encarga de aplicar el cambio en el DOM cuando el dato cambie.

Ejercicio 2 — Estados, no transiciones. Un componente SubmitButton debe verse de tres maneras según props.status ('idle', 'loading', 'done'): "Add to cart" habilitado, "Adding..." deshabilitado, "Added!" deshabilitado. Descríbelo declarativamente (sin pensar en transiciones). Pista: un dato de entrada, tres descripciones.

Ver solución
function SubmitButton(props) {
  const label =
    props.status === 'loading' ? 'Adding...' :
    props.status === 'done' ? 'Added!' :
    'Add to cart';
  const disabled = props.status !== 'idle';
  return (
    <button className="add-to-cart" disabled={disabled}>
      {label}
    </button>
  );
}

No describimos las transiciones ("cuando pase de idle a loading, cambia el texto y deshabilita"); describimos cómo se ve el botón para cada estado: el label sale de un ternario encadenado sobre status, y disabled es true salvo en 'idle'. Un solo dato de entrada (status), tres apariencias posibles, todas descritas. React va de una a otra cuando status cambie (lo cambiará el estado, módulo 3, y el evento, módulo 4).

Ejercicio 3 — ¿Por qué no se mutó el dato? En el ejemplo trabajado, soldOut = { ...inStock, inStock: false } y al final inStock.inStock seguía siendo true. Explica qué hace { ...inStock, inStock: false } y por qué importa que ProductCard no mute sus props para el modelo declarativo.

Ver solución

{ ...inStock, inStock: false } crea un objeto nuevo: copia todas las propiedades de inStock (con el spread ...inStock) y luego sobrescribe inStock con false. El original inStock no se toca —por eso al final inStock.inStock sigue siendo true—; soldOut es una copia independiente con un solo campo distinto.

Importa porque el modelo declarativo se apoya en que los componentes sean funciones puras: dado un dato, producen una descripción, sin modificar el dato ni nada de afuera. Si ProductCard mutara sus props, dos renders con "el mismo" dato podrían dar resultados distintos (el dato habría cambiado por debajo), y UI = f(datos) dejaría de cumplirse. Al describir con datos nuevos en vez de mutar los viejos, cada render depende solo de su entrada, y puedes razonar sobre "cómo se ve para estos datos" con total confianza. El módulo 3 convierte esto en regla dura: el estado nunca se muta, se reemplaza.

Resumen y siguiente paso

En esta lección cerraste el módulo con la idea que lo une: React es declarativo. No manipulas el DOM a mano —nada de querySelector, createElement, appendChild dentro de un componente—: describes cómo se ve la UI para los datos actuales, y React hace los cambios. Viste el contraste en código (la secuencia imperativa de pasos frente a la descripción declarativa) y lo mediste: re-describir un producto que pasa de en stock a agotado no fue editar el primer render, sino llamar de nuevo a la función con un dato nuevo y obtener una descripción entera —el badge "Sold out" apareció solo—, sin tocar ninguna pantalla y sin mutar el dato original (ProductCard es puro). Y reconociste que todo el módulo —el condicional, las listas, la key— son formas de describir, no de manipular.

Antes de avanzar deberías poder: distinguir el estilo imperativo (dar órdenes para cambiar la pantalla) del declarativo (describir cómo se ve para los datos); explicar por qué describir es más simple (razonas sobre estados, no transiciones; la verdad vive en los datos, no en el DOM); y describir un cambio con datos nuevos en vez de mutar los viejos.

La lección 8 es el mini-proyecto: ensamblas el catálogo del storefront de Mercado de punta a punta con JSX real, aplicando todo el módulo a la vez —el SearchBar con sus atributos, el ProductList que hace .map() con key, el badge "Sold out" con &&, y el "sin resultados" con early return— y ejecutas la lógica en Node para ver el HTML completo, con datos y con la lista vacía. Después de eso, el módulo 3 le dará vida: el estado hará que ese "cambia el dato y re-describe" ocurra solo, en respuesta a lo que haga el usuario.

Recursos