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 sí 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
ifafuera del JSX resuelve "sin resultados" y termina. - (2) Caso con datos →
.map()dentro del<section>(la lección 5 lo profundiza; nota lakey={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
- React, "Conditional Rendering" — react.dev/learn/conditional-rendering. La página oficial sobre
&&, el ternario y devolvernull; el centro de esta lección, incluida la advertencia del0con&&. En inglés. - React, "JavaScript in JSX with Curly Braces" — react.dev/learn/javascript-in-jsx-with-curly-braces. Por qué el ternario y el
&&caben en el JSX (son expresiones) y elifno. En inglés. - MDN, "Logical AND (
&&)" — developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_AND. Cómo funciona el&&en JavaScript (devuelve un operando, no siempre un booleano); explica la trampa del0. En inglés. - MDN, "Conditional (ternary) operator" — developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_operator. La referencia del
? :, la versión-expresión delif/else. En inglés.