Módulo 2: Jsx And Rendering
JSX es JavaScript: expresiones y una sola raíz
Descripción
La lección 1 enunció la tesis del módulo: JSX es JavaScript, azúcar sintáctico que compila a llamadas a función. Esta lección la clava con código corriendo y saca de ella tres consecuencias prácticas que usarás todos los días.
La primera: dentro de las llaves {} va una expresión de JavaScript. No un lenguaje de plantillas, no cualquier cosa: una expresión, que es JavaScript que produce un valor. Distinguir una expresión de una sentencia —que no produce un valor, sino que controla el flujo o declara algo— es la llave que abre la mitad de las reglas de JSX, incluida la pregunta "¿por qué no puedo poner un if dentro del JSX?".
La segunda: un componente devuelve una sola raíz. No es un capricho: un componente es una función, y una función devuelve un valor. Si quieres devolver dos etiquetas hermanas, tienes que envolverlas en algo que sea uno.
La tercera, que resuelve el problema que abre la segunda: el fragmento, una forma de agrupar varios hijos en una sola raíz sin meter un nodo extra (un <div> de más) en el árbol.
Conexión con el módulo. Esta lección es la base sobre la que se paran las otras. El renderizado condicional (lección 4) funciona porque el ternario y el && son expresiones que caben dentro de las llaves. Las listas (lección 5) funcionan porque .map() devuelve un array, que es un valor que las llaves aceptan. La regla de la sola raíz reaparece cada vez que un componente arma su marcado. Y los fragmentos vuelven en el proyecto (lección 8), cuando ensamblas piezas que no quieren un contenedor extra. Todo lo que sigue asume que estas tres consecuencias ya son reflejo.
Una analogía: el molde que produce una sola pieza
Imagina un molde de inyección en una fábrica: le metes material fundido y produce una pieza. Un molde no escupe tres piezas sueltas a la vez; produce una forma, aunque esa forma tenga muchos detalles —una carcasa con sus botones, sus ranuras, su tapa, todo de una pieza—. Si necesitas que salgan "dos cosas" juntas, el molde tiene que estar diseñado para que esas dos cosas sean una sola pieza con dos partes.
Un componente es ese molde. Le metes props (el material) y produce un elemento (la pieza). Puede ser un elemento riquísimo —un <article> con su <h3>, su <p>, su <span> anidados— pero es una pieza con muchos detalles adentro. Lo que un molde no puede hacer es escupir dos piezas independientes de un solo golpe; y un componente no puede devolver dos etiquetas hermanas sueltas. Si las necesitas juntas, las envuelves para que sean una: en un contenedor real (un <div>) o, cuando no quieres el contenedor de más, en un fragmento —el equivalente a un "sujetador invisible" que agrupa las partes en una pieza sin dejar marca en el producto final—.
Y los huecos del molde —el material fundido que le metes— son las llaves {}: aceptan lo que fluye y toma forma (una expresión, un valor), no un conjunto de instrucciones (una sentencia). Guarda las dos imágenes: una pieza a la vez (la sola raíz), y huecos que aceptan material que fluye (las expresiones).
Expresión vs sentencia: la distinción que lo explica todo
Antes del ejemplo, fijemos la distinción, porque de ella salen las reglas. En JavaScript hay dos categorías de código:
- Una expresión es cualquier fragmento que produce un valor.
2 + 2produce4.formatPrice(2599)produce'$25.99'.product.nameproduce'Wireless Mouse'.product.inStock ? 'In stock' : 'Out of stock'produce uno de los dos strings.products.map(p => ...)produce un array. Todas son expresiones: las podrías poner a la derecha de un=. - Una sentencia no produce un valor; hace algo (controla el flujo, declara, asigna).
if (x) { ... }es una sentencia: dirige el flujo, pero no "vale" nada; no puedes escribirconst y = if (x) {...}.for (...) { ... }es una sentencia.const z = 3;es una sentencia (la declaración). Unreturn, una sentencia.
La regla de las llaves de JSX cae sola de aquí: dentro de {} va una expresión, porque el hueco espera un valor para meterlo en el árbol. Un if o un for no producen un valor, así que no caben. No es que React los "prohíba" por gusto: es que no hay ningún valor que meter. Cuando necesites decidir dentro del JSX, usarás la versión-expresión de la decisión: el ternario ? : (lección 4). Y cuando necesites un bucle, usarás la versión-expresión del bucle: .map() (lección 5), que produce un array. El patrón es siempre el mismo: si necesitas lógica dentro del JSX, usa su forma de expresión; si necesitas una sentencia (un if, un for), ponla antes del return, no dentro del JSX.
Ejemplo trabajado: el árbol, las expresiones y el fragmento
Vamos a ejecutar las tres consecuencias de una vez. Primero, el ProductCard 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>
<span className="product-stock">
{props.inStock ? 'In stock' : 'Out of stock'}
</span>
</article>
);
}
Nota la sola raíz: todo lo que devuelve ProductCard está envuelto en un <article>. Y nota los huecos: {props.name} (una expresión: acceso a propiedad), {formatPrice(props.priceCents)} (una expresión: llamada a función), {props.inStock ? ... : ...} (una expresión: ternario). Ninguna es una sentencia.
Ahora la versión ejecutable en Node. h modela createElement; el ProductCard con h es lo mismo que el JSX, solo que explícito. Agregamos un Fragment al renderToString para la última parte:
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) { // el fragmento no pinta un nodo: solo agrupa
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);
}
// ProductCard escrito con h: es EXACTAMENTE a lo que compila el JSX.
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')
);
}
const mouse = { name: 'Wireless Mouse', priceCents: 2599, inStock: true };
console.log('=== 1) El JSX compila a un arbol de objetos (UI = datos) ===');
console.log(JSON.stringify(ProductCard(mouse), null, 2));
console.log('\n=== 2) Dentro de {} corre JavaScript de verdad ===');
console.log('{formatPrice(2599)} ->', formatPrice(2599));
console.log('{2599 > 5000} ->', 2599 > 5000);
console.log("{mouse.inStock ? 'In' : 'Out'} ->", mouse.inStock ? 'In stock' : 'Out of stock');
console.log("{mouse.name.toUpperCase()} ->", mouse.name.toUpperCase());
console.log('\n=== 3) Un componente devuelve UNA sola raiz ===');
console.log(renderToString(ProductCard(mouse)));
console.log('\n=== 4) Fragmento: agrupar dos hermanos sin un nodo extra ===');
function PriceAndStock(props) {
return h(Fragment, {},
h('p', { className: 'product-price' }, formatPrice(props.priceCents)),
h('span', { className: 'product-stock' },
props.inStock ? 'In stock' : 'Out of stock')
);
}
console.log(renderToString(PriceAndStock(mouse)));
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== 1) El JSX compila a un arbol de objetos (UI = datos) ===
{
"tag": "article",
"props": {
"className": "product-card"
},
"children": [
{
"tag": "h3",
"props": {
"className": "product-name"
},
"children": [
"Wireless Mouse"
]
},
{
"tag": "p",
"props": {
"className": "product-price"
},
"children": [
"$25.99"
]
},
{
"tag": "span",
"props": {
"className": "product-stock"
},
"children": [
"In stock"
]
}
]
}
=== 2) Dentro de {} corre JavaScript de verdad ===
{formatPrice(2599)} -> $25.99
{2599 > 5000} -> false
{mouse.inStock ? 'In' : 'Out'} -> In stock
{mouse.name.toUpperCase()} -> WIRELESS MOUSE
=== 3) Un componente devuelve UNA sola raiz ===
<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>
=== 4) Fragmento: agrupar dos hermanos sin un nodo extra ===
<p class="product-price">$25.99</p>
<span class="product-stock">In stock</span>
Lee las cuatro partes; cada una es una de las consecuencias.
Parte 1: JSX es datos. ProductCard(mouse) no devolvió HTML: devolvió un objeto —un árbol de { tag, props, children } anidados—. La UI, otra vez, es datos antes de ser HTML (lo viste en el módulo 1; aquí lo confirmas con JSX real). Fíjate que las props se llaman className en el objeto —el nombre de JavaScript—, y que los textos ya están evaluados: "$25.99", no formatPrice(...). Las expresiones de los huecos corrieron al construirse el árbol.
Parte 2: dentro de {} corre JavaScript. Las cuatro líneas prueban que el hueco no es un lenguaje de plantillas: es JavaScript real. Una llamada a función (formatPrice(2599) → $25.99), una comparación (2599 > 5000 → false), un ternario (→ In stock), un método de string (toUpperCase() → WIRELESS MOUSE). Todas son expresiones: todas produjeron un valor. Cualquiera de ellas cabe en un {}.
Parte 3: una sola raíz. ProductCard devolvió un <article> con todo adentro. No devolvió el <h3>, el <p> y el <span> sueltos: los envolvió en una raíz. Es la consecuencia de que un componente es una función que devuelve un valor.
Parte 4: el fragmento. PriceAndStock necesitaba devolver dos hermanos —un <p> y un <span>— sin un contenedor de más. Un fragmento hizo justo eso: en la salida están el <p> y el <span> al mismo nivel, sin ningún <div> envolviéndolos. El fragmento agrupó para satisfacer "una sola raíz", pero no dejó rastro en el HTML. Compáralo con la parte 3: ahí el <article> sí aparece (es un nodo real que queremos); aquí no queríamos un nodo extra, y el fragmento nos dio la agrupación sin el nodo.
Profundización: por qué el fragmento existe
Podrías preguntarte: si necesito una sola raíz, ¿por qué no envuelvo siempre en un <div> y ya? A veces se puede. Pero hay casos donde un <div> de más rompe algo: dentro de una <table>, un <div> entre el <tbody> y los <tr> es HTML inválido; en un layout con flexbox o grid, un contenedor extra altera el diseño porque agrega un nivel que el CSS no esperaba; y en general, un árbol lleno de <div> sin propósito ("divitis") es más difícil de leer y de estilar. El fragmento resuelve exactamente eso: te da la agrupación que la regla de la sola raíz exige, sin el nodo del DOM que un <div> metería.
En JSX real, el fragmento tiene dos formas. La explícita, <Fragment>...</Fragment> (importando Fragment de React), y la corta, <>...</> —dos etiquetas vacías—:
function PriceAndStock(props) {
return (
<>
<p className="product-price">{formatPrice(props.priceCents)}</p>
<span className="product-stock">
{props.inStock ? 'In stock' : 'Out of stock'}
</span>
</>
);
}
Las <> y </> son el fragmento: agrupan el <p> y el <span> en una sola raíz, y al renderizar no producen ningún elemento propio —igual que viste en la parte 4—. Usa la forma corta <> casi siempre; la forma larga <Fragment key={...}> solo cuando necesitas darle una key (por ejemplo, en una lista de fragmentos, tema de la lección 6).
Profundización: qué NO es una expresión (y qué hacer)
La regla "dentro de {} va una expresión" tiene una cara práctica: la lógica que no es una expresión va antes del return. Ejemplo. Supón que quieres elegir una etiqueta de descuento según el precio. La tentación es meter un if en el JSX; no cabe. Lo correcto es calcular antes, en una variable, y meter la variable (que sí es una expresión) en el hueco:
function ProductCard(props) {
// La logica (sentencias) va ANTES del return.
let dealLabel;
if (props.priceCents < 3000) {
dealLabel = 'Budget pick';
} else {
dealLabel = 'Premium';
}
// El JSX solo tiene EXPRESIONES en los huecos.
return (
<article className="product-card">
<h3 className="product-name">{props.name}</h3>
<span className="deal">{dealLabel}</span>
</article>
);
}
El if vive arriba, como sentencia, donde le corresponde. Al return llega ya resuelto en dealLabel, y el hueco {dealLabel} mete esa variable —una expresión—. Esta separación —sentencias arriba, expresiones en los huecos— es una de las costumbres que más ordena tu código React. Cuando algo no cabe en un {}, no fuerces: súbelo antes del return.
Errores comunes
Poner un if o un for dentro del JSX. Qué pasa: se intenta {if (props.inStock) { ... }} o {for (...) { ... }} dentro de las llaves, y no compila. Por qué pasa: es lo natural si piensas en las llaves como "un lugar para poner lógica". Cómo detectarlo: error de sintaxis al compilar, apuntando a la llave. Cómo corregirlo: recuerda que en {} va una expresión, no una sentencia. Para condicionar dentro del JSX usa el ternario ? : o && (lección 4); para iterar usa .map() (lección 5). Y si de verdad necesitas un if o un for, ponlos antes del return y mete el resultado (una variable) en el hueco. La regla mecánica: si no lo puedes escribir a la derecha de un =, no cabe en un {}.
Devolver dos etiquetas hermanas sin envolver. Qué pasa: un componente hace return (<h3>...</h3> <span>...</span>) —dos hermanas sueltas— y falla. Por qué pasa: uno piensa en "devolver el marcado", no en "devolver un valor". Cómo detectarlo: error del tipo "JSX expressions must have one parent element" / "Adjacent JSX elements must be wrapped". Cómo corregirlo: envuelve en una sola raíz. Si el contenedor tiene sentido semántico (un <article>, un <section>), úsalo. Si solo lo pondrías para satisfacer la regla, usa un fragmento <>...</>: agrupa sin meter un nodo extra. La causa es que un componente es una función y devuelve un valor; dos hermanas son dos valores.
Envolver todo en <div> por reflejo (divitis). Qué pasa: para satisfacer "una sola raíz" se mete un <div> alrededor de todo, siempre, y el árbol se llena de contenedores sin propósito. Por qué pasa: es la solución más obvia y funciona a primera vista. Cómo detectarlo: tienes <div> que no aportan estructura ni estilo, solo "envuelven"; o un layout con flexbox/grid se descuadra por un contenedor de más; o metes un <div> dentro de una tabla y el HTML queda inválido. Cómo corregirlo: cuando solo necesitas agrupar (no un contenedor real), usa el fragmento <>...</>. No pinta ningún nodo, así que no altera el DOM, el layout ni la validez del HTML. Reserva el <div> para cuando de verdad quieres un contenedor.
Ejercicios
Ejercicio 1 — Expresión o sentencia. Para cada fragmento, di si es una expresión (cabe en un {} del JSX) o una sentencia (no cabe, va antes del return): (a) product.priceCents / 100; (b) if (product.inStock) label = 'yes';; (c) products.filter(p => p.inStock); (d) const total = 0;; (e) product.inStock && 'Available'.
Ver solución
- (a)
product.priceCents / 100— Expresión. Una división produce un valor (un número). Cabe en{}. - (b)
if (product.inStock) label = 'yes';— Sentencia. Unifcontrola el flujo; no produce un valor. Va antes delreturn. (La versión-expresión sería un ternario.) - (c)
products.filter(p => p.inStock)— Expresión..filter()devuelve un array; ese array es un valor. Cabe en{}(aunque un array de productos crudos rara vez es lo que quieres renderizar; normalmente le encadenas un.map()). - (d)
const total = 0;— Sentencia. Una declaración; no produce un valor. Va antes delreturn. - (e)
product.inStock && 'Available'— Expresión. El&&produce un valor ('Available'ofalse). Cabe en{}; es justo el patrón del renderizado condicional de la lección 4.
La regla mecánica que confirma todas: ¿lo podrías escribir a la derecha de un =? Si sí, es expresión y cabe. const x = product.priceCents / 100; funciona (a). const x = if (...) no funciona (b).
Ejercicio 2 — Arregla el componente. Este componente no compila. Identifica los dos problemas y reescríbelo correctamente:
function ProductHeader(props) {
return (
<h3 className="product-name">{props.name}</h3>
<span className="product-stock">
{if (props.inStock) { 'In stock' } else { 'Out of stock' }}
</span>
);
}
Ver solución
Dos problemas: (1) devuelve dos etiquetas hermanas sueltas (el <h3> y el <span>) sin una sola raíz; (2) mete un if/else (una sentencia) dentro de las llaves, donde solo caben expresiones.
Arreglado con un fragmento y un ternario:
function ProductHeader(props) {
return (
<>
<h3 className="product-name">{props.name}</h3>
<span className="product-stock">
{props.inStock ? 'In stock' : 'Out of stock'}
</span>
</>
);
}
El fragmento <>...</> agrupa las dos hermanas en una sola raíz sin meter un nodo extra. Y el ternario props.inStock ? 'In stock' : 'Out of stock' es la versión-expresión del if/else: produce un valor, así que cabe en el hueco.
Ejercicio 3 — Predice el árbol y el HTML. Sin correr nada: (a) escribe el objeto que devuelve h('span', { className: 'badge' }, 'New'); (b) di qué HTML produce renderToString sobre él; (c) si en vez de h('span', ...) fuera un fragmento con dos hijos, h(Fragment, {}, h('span', {}, 'A'), h('span', {}, 'B')), ¿qué diferencia habría en el HTML respecto a envolver los dos <span> en un <div>?
Ver solución
(a) El objeto:
{ tag: 'span', props: { className: 'badge' }, children: [ 'New' ] }
(b) El HTML:
<span class="badge">New</span>
(c) Con el fragmento, el HTML son los dos <span> al mismo nivel, sin contenedor:
<span>A</span>
<span>B</span>
Con un <div> envolviéndolos, habría un nodo extra:
<div>
<span>A</span>
<span>B</span>
</div>
La diferencia es ese <div>. El fragmento satisface "una sola raíz" (agrupa los dos <span> en una cosa que el componente puede devolver) pero no produce ningún elemento en el DOM; el <div> sí. Cuando el contenedor no aporta nada, el fragmento evita ensuciar el árbol.
Resumen y siguiente paso
En esta lección convertiste la tesis "JSX es JavaScript" en tres herramientas de todos los días. Una: dentro de {} va una expresión —JavaScript que produce un valor—, y la distinción expresión vs sentencia explica por qué un if o un for no caben dentro del JSX (van antes del return) mientras que un ternario, una llamada a función o un .map() sí. Dos: un componente devuelve una sola raíz, porque es una función que devuelve un valor. Tres: el fragmento <>...</> agrupa varios hijos en una sola raíz sin meter un nodo extra —lo mediste: el <p> y el <span> salieron al mismo nivel, sin envoltorio—. Y viste, ejecutado, que un ProductCard en JSX compila a un árbol de objetos con las expresiones de los huecos ya evaluadas.
Antes de avanzar deberías poder: distinguir una expresión de una sentencia y decir cuál cabe en un {}; explicar por qué un componente devuelve una sola raíz; y usar un fragmento en vez de un <div> cuando el contenedor no aporta nada.
La lección 3 toma la primera "regla rara" concreta de los atributos: por qué se escribe className y no class, htmlFor y no for. La respuesta ya la intuyes —son palabras reservadas de JavaScript, y como el JSX es JS, chocarían— y la vas a ver ejecutada: el SearchBar de Mercado con su <label htmlFor> y su <input>, viendo renderToString traducir los nombres de JavaScript a los del HTML, más los atributos cuyo valor es una expresión y los atributos booleanos como disabled.
Recursos
- React, "JavaScript in JSX with Curly Braces" — react.dev/learn/javascript-in-jsx-with-curly-braces. Qué expresiones caben dentro de las llaves y por qué; el centro de esta lección. En inglés.
- React, "Writing Markup with JSX" — react.dev/learn/writing-markup-with-jsx. Las reglas de JSX (una sola raíz, cerrar etiquetas) explicadas por la doc oficial, incluida la razón de envolver hermanos. En inglés.
- React,
<Fragment>(<>...</>) — react.dev/reference/react/Fragment. La referencia del fragmento: cuándo usarlo, la forma corta<>y cuándo hace falta la larga conkey. En inglés. - MDN, "Expressions and operators" — developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators. La referencia de JavaScript sobre qué es una expresión; afina la distinción expresión vs sentencia. En inglés.