Módulo 2: Jsx And Rendering

Renderizado condicional: `&&`, ternario, early return

Descripción

Hasta ahora, cada componente que escribiste devuelve siempre la misma estructura, solo con datos distintos en los huecos. Pero una interfaz real muestra u oculta partes según los datos: un producto agotado lleva un badge "Sold out" que uno en stock no lleva; un catálogo con resultados muestra las tarjetas, y uno vacío muestra "No products found". Eso es el renderizado condicional: describir qué parte de la UI existe —o no— según una condición.

Y aquí está la clave que conecta con todo el módulo: como dentro del JSX solo caben expresiones (lección 2), el condicional se hace con las versiones-expresión de "decidir". Son tres, y cada una tiene su caso:

  • && (el "y" lógico) para "muéstralo solo si": una parte que existe o no existe, sin alternativa. El badge "Sold out".
  • Ternario ? : para "esto o aquello": siempre se muestra una de dos opciones. El texto "In stock" / "Out of stock".
  • Early return para "si no hay nada, describe el estado vacío y termina": una rama completa distinta antes de llegar al JSX principal. El "No products found".

Conexión con el módulo. El condicional se para sobre la lección 2: && y ? : son expresiones, por eso caben en las llaves; un if no cabría. Se apoya en la lección 3: el badge y el estado vacío llevan sus atributos (className). Y prepara la lección 5: el "sin resultados" convive con la lista .map() —muestras la lista o el estado vacío—. Todo sigue estático: la condición la fija el dato que le pasamos al componente; que la condición cambie sola (el producto se agota en vivo) es el estado, módulo 3.

Una analogía: las tres formas de decidir qué va en la caja

Imagina que empacas una caja de producto para enviar, y tienes tres tipos de decisión según el caso.

Un extra opcional. Algunos pedidos llevan una muestra gratis; otros, no. La regla es: "si el pedido califica, mete la muestra; si no, no metas nada extra". No hay un "en vez de la muestra, mete otra cosa": o va, o no va nada. Eso es el &&: pedidoCalifica && muestraGratis. Si califica, se mete la muestra; si no, se mete "nada" (nada visible).

Una etiqueta que siempre va, pero cambia. Cada caja lleva una etiqueta de estado: o dice "Frágil" o dice "Normal", pero siempre lleva una de las dos —nunca va sin etiqueta—. Eso es el ternario: esFragil ? 'Frágil' : 'Normal'. Siempre sale una de las dos ramas; nunca "ninguna".

Una caja vacía que ni se arma. Si el pedido no tiene productos, no tiene sentido armar la caja con sus separadores y su relleno: pones una nota "Pedido vacío" y terminas ahí, sin montar nada más. Eso es el early return: revisas la condición al principio, y si aplica, devuelves la versión vacía y no sigues.

Guarda las tres: extra opcional (&&), etiqueta que siempre va pero cambia (ternario), y caja que ni se arma (early return). Elegir bien entre ellas es casi todo el renderizado condicional.

Ejemplo trabajado: el badge "Sold out" y el "sin resultados"

Vamos a ejecutar las tres. Primero, en JSX real —la API que escribirás—:

function ProductCard(props) {
  return (
    <article className="product-card">
      <h3 className="product-name">{props.name}</h3>
      <p className="product-price">{formatPrice(props.priceCents)}</p>
      {/* ternario: SIEMPRE hay texto de estado, elige cual */}
      <span className="product-stock">
        {props.inStock ? 'In stock' : 'Out of stock'}
      </span>
      {/* &&: el badge SOLO existe si el producto esta agotado */}
      {!props.inStock && <span className="badge badge-out">Sold out</span>}
    </article>
  );
}

function ProductList(props) {
  // early return: si no hay productos, describe el estado vacio y termina.
  if (props.products.length === 0) {
    return <p className="empty-state">No products found</p>;
  }
  return (
    <section className="product-list">
      {props.products.map((p) => (
        <ProductCard key={p.id} name={p.name} priceCents={p.priceCents} inStock={p.inStock} />
      ))}
    </section>
  );
}

Nota las tres formas juntas. El ternario en el <span> del stock: siempre sale "In stock" o "Out of stock". El && en el badge: {!props.inStock && <span>...</span>} —si el producto está en stock, !props.inStock es false, y no se muestra nada; si está agotado, se muestra el badge—. Y el early return en ProductList: si la lista está vacía, devuelve el <p> del estado vacío antes de llegar al <section> con las tarjetas —el if va afuera del JSX, como sentencia, donde le corresponde (lección 2)—.

Ahora la versión ejecutable en Node:

function h(tag, props, ...children) {
  return { tag, props: props || {}, children: children.flat() };
}
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 '';   // false/null no pintan nada
  if (typeof node !== 'object') return pad + node;
  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 ProductCard(props) {
  return h('article', { className: 'product-card' },
    h('h3', { className: 'product-name' }, props.name),
    h('p', { className: 'product-price' }, formatPrice(props.priceCents)),
    // ternario: SIEMPRE hay texto de estado, elige cual
    h('span', { className: 'product-stock' },
      props.inStock ? 'In stock' : 'Out of stock'),
    // &&: el badge SOLO existe si el producto esta agotado
    !props.inStock && h('span', { className: 'badge badge-out' }, 'Sold out')
  );
}

// early return: si no hay productos, describe el estado vacio y termina.
function ProductList(props) {
  if (props.products.length === 0) {
    return h('p', { className: 'empty-state' }, 'No products found');
  }
  return h('section', { className: 'product-list' },
    props.products.map((p) => ProductCard(p))
  );
}

const products = [
  { id: 'p1', name: 'Wireless Mouse', priceCents: 2599, inStock: true },
  { id: 'p2', name: 'Mechanical Keyboard', priceCents: 8900, inStock: false },
];

console.log('=== 1) El badge "Sold out" con && (solo si !inStock) ===');
console.log('ProductCard(mouse, en stock):');
console.log(renderToString(ProductCard(products[0])));
console.log('\nProductCard(keyboard, agotado):');
console.log(renderToString(ProductCard(products[1])));

console.log('\n=== 2) "Sin resultados": lista vacia vs con datos (early return) ===');
console.log('ProductList con [] (vacia):');
console.log(renderToString(ProductList({ products: [] })));
console.log('\nProductList con 2 productos:');
console.log(renderToString(ProductList({ products })));

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

=== 1) El badge "Sold out" con && (solo si !inStock) ===
ProductCard(mouse, en stock):
<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>

ProductCard(keyboard, agotado):
<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>
</article>

=== 2) "Sin resultados": lista vacia vs con datos (early return) ===
ProductList con [] (vacia):
<p class="empty-state">No products found</p>

ProductList con 2 productos:
<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>
    <span class="badge badge-out">Sold out</span>
  </article>
</section>

Lee las dos partes; en ellas están las tres herramientas.

Parte 1: el && y el ternario. Compara las dos tarjetas. El ternario hizo su trabajo en ambas: el mouse muestra "In stock", el teclado "Out of stock" —siempre uno de los dos, nunca ninguno—. El && se comportó distinto en cada una: el mouse (inStock: true) no tiene badge —!true es false, y false && ... no produce nada, así que no hay <span class="badge">—; el teclado (inStock: false) sí lo tiene!false es true, y true && <span>...</span> produce el badge—. Esa es la diferencia entre las dos herramientas: el ternario elige cuál de dos textos; el && decide si una parte existe o no.

Cómo funciona el && por dentro, porque conviene tenerlo claro: en JavaScript, false && loQueSea evalúa a false sin siquiera mirar el lado derecho; true && x evalúa a x. Así que !props.inStock && <badge> da false (cuando hay stock) o el elemento <badge> (cuando no). Y nuestro renderToString —igual que React— trata el false como "no pintes nada" (la primera línea: typeof node === 'boolean'''). Por eso el badge aparece o desaparece limpio.

Parte 2: el early return. Con la lista vacía, ProductList devolvió solo el <p class="empty-state">No products found</p> —ni rastro del <section> ni de tarjetas—. Con dos productos, devolvió el <section> con las dos tarjetas y sin el estado vacío. El if (length === 0) al principio de la función eligió la rama completa: si no hay nada, sale por la puerta del estado vacío y ni llega al JSX de la lista. Es la forma más limpia de manejar "no hay datos": no ensucias el JSX principal con condiciones, resuelves el caso vacío arriba y terminas.

Profundización: cuál elegir, y la trampa del && con números

La regla para elegir. Pregúntate qué forma tiene la decisión:

  • ¿Una parte que existe o no, sin alternativa? → &&. (El badge, un aviso, un ícono opcional.)
  • ¿Siempre una de dos (o más) opciones? → ternario ? :. (El texto de stock; un botón "Add" vs "Remove".)
  • ¿Una pantalla/rama completa distinta, sobre todo el caso "vacío" o "cargando"? → early return. (Sin resultados; sin sesión; cargando.)

No son excluyentes —un componente usa las tres a la vez, como viste—, pero elegir la que corresponde hace el código legible. Un ternario cuya segunda rama es null (cond ? <x/> : null) es un && disfrazado y más ruidoso; un && para elegir entre dos textos te obliga a dos && y se enreda.

La trampa del && con números. El && tiene un filo con el que todo el mundo se corta una vez. El lado izquierdo debe ser un booleano. Si pones un número, el 0 es un valor "falsy" (cuenta como falso), pero no es false: es el número 0, y React lo pinta. Ejemplo peligroso:

{props.stockCount && <span className="badge">Only {props.stockCount} left</span>}

Cuando stockCount es 0, 0 && <span>... evalúa a 0 —no a false—, y React renderiza el texto 0 suelto en la pantalla. En vez de "no mostrar nada", aparece un 0 fantasma. El arreglo es forzar un booleano de verdad:

{props.stockCount > 0 && <span className="badge">Only {props.stockCount} left</span>}

props.stockCount > 0 es false cuando el conteo es 0, y false no se pinta. La regla mecánica: a la izquierda de un && en JSX, pon siempre una condición booleana (x > 0, list.length > 0, Boolean(x)), no un número ni un string que podrían ser "falsy pero visibles". Por eso en el ejemplo trabajado usamos props.products.length === 0 (booleano) y no props.products.length a secas.

Errores comunes

Usar && con un número a la izquierda. Qué pasa: {count && <Badge/>} con count === 0 pinta un 0 suelto en la pantalla en vez de nada. Por qué pasa: uno asume que "falsy" y false son lo mismo para React; no lo son —el 0 es falsy pero visible—. Cómo detectarlo: aparece un 0 (o a veces NaN) fantasma donde esperabas vacío. Cómo corregirlo: pon a la izquierda del && una condición booleana: {count > 0 && <Badge/>}. Así el caso vacío da false, y false no se pinta. Aplica igual a list.length (usa list.length > 0).

Meter un if dentro del JSX en vez de antes del return. Qué pasa: se intenta {if (cond) { ... }} dentro de las llaves para el caso vacío, y no compila. Por qué pasa: es lo natural si piensas "aquí decido". Cómo detectarlo: error de sintaxis en la llave. Cómo corregirlo: para "esto o aquello" dentro del JSX usa el ternario (una expresión); para una rama completa (el estado vacío, el "cargando") usa un early return antes del JSX —el if afuera, como sentencia—. Nunca un if dentro de las llaves: no es una expresión.

Olvidar el estado vacío. Qué pasa: se renderiza products.map(...) sin manejar el caso de lista vacía, y cuando no hay resultados la pantalla queda en blanco —sin mensaje, sin nada—, lo que parece un bug. Por qué pasa: durante el desarrollo casi siempre hay datos, así que el caso vacío no se ve. Cómo detectarlo: filtras por algo que no existe (o los datos no cargan) y la sección queda muda. Cómo corregirlo: maneja el caso vacío explícitamente, casi siempre con un early return (if (list.length === 0) return <EmptyState/>). Un buen componente de lista describe tres estados, no uno: con datos, vacío, y —más adelante, con efectos— cargando. En este módulo, al menos los dos primeros.

Ejercicios

Ejercicio 1 — Elige la herramienta. Para cada requisito, di cuál de las tres formas (&&, ternario, early return) usarías y escribe la línea: (a) mostrar un ícono de "envío gratis" solo si props.freeShipping es true; (b) mostrar el texto del botón como "Remove" si el producto ya está en el carrito, o "Add to cart" si no; (c) si props.products está vacío, mostrar <EmptyCatalog /> en lugar de la lista.

Ver solución
  • (a) Existe o no, sin alternativa → &&:
    {props.freeShipping && <span className="free-shipping">Free shipping</span>}
  • (b) Siempre uno de dos textos → ternario:
    <button>{props.inCart ? 'Remove' : 'Add to cart'}</button>
  • (c) Una rama completa para el caso vacío → early return:
    if (props.products.length === 0) {
      return <EmptyCatalog />;
    }
    // ...luego el JSX de la lista

La forma sigue la decisión: opcional sin alternativa (&&), siempre una de dos (ternario), rama entera para el caso vacío (early return).

Ejercicio 2 — Caza la trampa del 0. Este código quiere mostrar "Only N left" cuando quedan pocas unidades, pero tiene el bug del &&. Explica qué pasa cuando stockCount es 0 y arréglalo:

<div className="stock-info">
  {product.stockCount && <span className="low-stock">Only {product.stockCount} left</span>}
</div>
Ver solución

Cuando product.stockCount es 0, la expresión 0 && <span>... evalúa a 0 —no a false, porque el && devuelve el lado izquierdo cuando este es falsy—. Y React pinta el 0: en la pantalla aparece un 0 suelto dentro del <div className="stock-info">, justo cuando no queda stock. El 0 es "falsy" pero visible.

Arreglo: forzar una condición booleana a la izquierda del &&.

<div className="stock-info">
  {product.stockCount > 0 && <span className="low-stock">Only {product.stockCount} left</span>}
</div>

product.stockCount > 0 es false cuando el conteo es 0, y false no se pinta. Regla: a la izquierda del &&, siempre un booleano de verdad.

Ejercicio 3 — Los tres estados del catálogo. Escribe (en JSX o en h) un ProductList que maneje tres casos: (1) si props.products es una lista vacía, muestra <p className="empty-state">No products found</p>; (2) si tiene productos, muestra la <section> con un ProductCard por producto; (3) además, cada ProductCard debe mostrar el badge "Sold out" solo para los agotados. Indica qué herramienta usa cada caso.

Ver solución
function ProductCard(props) {
  return (
    <article className="product-card">
      <h3 className="product-name">{props.name}</h3>
      <p className="product-price">{formatPrice(props.priceCents)}</p>
      {/* (3) && : badge solo si esta agotado */}
      {!props.inStock && <span className="badge badge-out">Sold out</span>}
    </article>
  );
}

function ProductList(props) {
  // (1) early return: caso vacio
  if (props.products.length === 0) {
    return <p className="empty-state">No products found</p>;
  }
  // (2) el caso con datos: map (leccion 5)
  return (
    <section className="product-list">
      {props.products.map((p) => (
        <ProductCard key={p.id} name={p.name} priceCents={p.priceCents} inStock={p.inStock} />
      ))}
    </section>
  );
}
  • (1) Caso vacío → early return. El if afuera del JSX resuelve "sin resultados" y termina.
  • (2) Caso con datos → .map() dentro del <section> (la lección 5 lo profundiza; nota la key={p.id}).
  • (3) Badge de agotado → &&. Existe solo cuando !props.inStock.

Tres estados, tres herramientas, cada una en su lugar.

Resumen y siguiente paso

En esta lección aprendiste el renderizado condicional: describir qué parte de la UI existe según los datos, usando las versiones-expresión de "decidir", que son las que caben en el JSX. && para "muéstralo solo si" —el badge "Sold out", que existe o no—; ternario ? : para "esto o aquello" —el texto "In stock"/"Out of stock", siempre uno—; y early return para "si no hay nada, describe el estado vacío y termina" —el "No products found"—. Lo mediste ejecutando el badge con un producto en stock (no aparece) y uno agotado (aparece), y el ProductList con lista vacía (estado vacío) y con datos (las tarjetas). Y esquivaste la trampa clásica: a la izquierda de un && va un booleano, nunca un número, porque el 0 es falsy pero visible.

Antes de avanzar deberías poder: elegir entre &&, ternario y early return según la forma de la decisión; explicar por qué el && con un número puede pintar un 0 fantasma y cómo evitarlo; y manejar el estado vacío de una lista con un early return.

La lección 5 profundiza la otra mitad del par que ya asomó: las listas con .map(). Vas a ver, ejecutado, cómo .map() transforma un array de productos en un array de ProductCard —un componente por dato— y cómo React lo aplana en la UI; el mismo patrón del ProductList que aquí solo mencionamos, ahora con lupa. Y ahí aparece, ya en serio, la key —que la lección 6 lleva hasta el fondo—.

Recursos