Módulo 2: Jsx And Rendering
Renderizar listas con `.map()`
Descripción
Una tienda no muestra un producto: muestra todos los que hay, cada uno en su tarjeta. Y no sabes de antemano cuántos son —hoy tres, mañana trescientos, a veces cero—. No vas a escribir trescientos <ProductCard /> a mano. Necesitas una forma de decir "por cada producto de esta lista, un ProductCard", y que funcione para cualquier cantidad. Esa forma es .map(), y es el patrón que verás más veces en tu vida como desarrollador React.
.map() es un método de los arrays de JavaScript —no algo de React—: toma un array y devuelve otro array, aplicando una función a cada elemento. Si le das un array de datos (productos) y una función que convierte un dato en un elemento (ProductCard), te devuelve un array de elementos, uno por dato. Y React sabe renderizar un array de elementos: los pinta en orden, uno tras otro. Ese es todo el mecanismo. Ya lo viste asomar en la composición del módulo 1 y en el ProductList de la lección 4; aquí lo miramos con lupa.
Conexión con el módulo. Las listas son la otra pieza grande del renderizado, junto con el condicional (lección 4) —de hecho, conviven: muestras la lista o el estado vacío—. .map() funciona en el JSX porque devuelve un valor (un array), y las llaves aceptan expresiones (lección 2). Y esta lección deja planteada, a propósito sin resolver del todo, la pregunta más importante de las listas: cada elemento necesita una key. Aquí verás qué es y dónde va; la lección 6 explica por qué debe ser estable y qué se rompe con el índice. Todo sigue estático: la lista de productos nos la dan; de dónde sale (filtrada por la búsqueda) es el módulo 5.
Una analogía: la línea de producción con un molde
Vuelve al molde de la lección 2 —el que produce una pieza a partir de material—. Ahora ponlo en una línea de producción. Por la cinta llega una fila de materiales distintos: un lote de plástico rojo, uno azul, uno verde. El molde es el mismo, pero por cada material que le llega, produce una pieza de ese color. Entra una fila de materiales; sale una fila de piezas, una por material.
Eso es .map() con un componente. La cinta de materiales es tu array de productos (los datos). El molde es tu componente (ProductCard). Y .map() es la línea que pasa cada material por el molde: products.map(product => ProductCard(product)) toma cada producto y produce su tarjeta. Entra un array de datos, sale un array de elementos, uno por dato. La belleza es que la línea no sabe ni le importa cuántos materiales llegan: si llegan tres, salen tres piezas; si llegan cien, cien; si no llega ninguno, la cinta sale vacía. No hay nada "cableado" a un número.
El giro de la analogía: la fila de piezas que sale de la línea todavía no está en la caja de envío —es un array de piezas—. Alguien tiene que tomarlas y acomodarlas en la caja. En React, ese "acomodar" lo hace el propio JSX: cuando pones el array dentro de un <section>, React lo aplana y pinta cada pieza en orden. Guarda la imagen: material entra, molde, fila de piezas sale, y el contenedor las acomoda.
Ejemplo trabajado: ProductList recorre el array
Vamos a ejecutar el patrón y, sobre todo, a ver qué devuelve .map() antes de renderizarlo —porque entender que devuelve un array de elementos es la mitad del asunto—.
Primero, en JSX real —la API que escribirás—:
function ProductList(props) {
return (
<section className="product-list">
{props.products.map((product) => (
<ProductCard
key={product.id}
name={product.name}
priceCents={product.priceCents}
inStock={product.inStock}
/>
))}
</section>
);
}
Léelo con calma. Dentro del <section>, las llaves {...} abren una expresión de JavaScript: props.products.map(...). El .map() recorre props.products y, por cada product, devuelve un <ProductCard /> con las props de ese producto. El resultado es un array de elementos <ProductCard />, que React pinta dentro del <section>, en orden. Fíjate en el key={product.id}: cada elemento de la lista lleva una key —la identidad del producto—. Por ahora acéptala como requisito; la lección 6 la explica a fondo.
Ahora la versión ejecutable en Node, donde primero inspeccionamos lo que devuelve .map() y luego lo renderizamos:
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 '';
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)),
h('span', { className: 'product-stock' },
props.inStock ? 'In stock' : 'Out of stock')
);
}
const products = [
{ id: 'p1', name: 'Wireless Mouse', priceCents: 2599, inStock: true },
{ id: 'p2', name: 'Mechanical Keyboard', priceCents: 8900, inStock: false },
{ id: 'p3', name: 'USB-C Hub', priceCents: 3499, inStock: true },
];
// .map transforma cada producto (dato) en un ProductCard (vnode).
const cards = products.map((p) => ProductCard(p));
console.log('=== 1) .map devuelve un ARRAY: un vnode por producto ===');
console.log('products.length =', products.length);
console.log('cards.length =', cards.length);
console.log('cards[0].tag =', cards[0].tag);
console.log('cards[0].props =', JSON.stringify(cards[0].props));
function ProductList(props) {
return h('section', { className: 'product-list' },
props.products.map((p) => ProductCard(p))
);
}
console.log('\n=== 2) Ese array de vnodes, renderizado a HTML ===');
console.log(renderToString(ProductList({ products })));
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== 1) .map devuelve un ARRAY: un vnode por producto ===
products.length = 3
cards.length = 3
cards[0].tag = article
cards[0].props = {"className":"product-card"}
=== 2) Ese array de vnodes, renderizado a HTML ===
<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>
</article>
<article class="product-card">
<h3 class="product-name">USB-C Hub</h3>
<p class="product-price">$34.99</p>
<span class="product-stock">In stock</span>
</article>
</section>
Lee las dos partes; juntas explican el patrón.
Parte 1: .map() devuelve un array. Antes de renderizar nada, inspeccionamos cards, el resultado de products.map(p => ProductCard(p)). Tres datos entraron (products.length = 3), tres elementos salieron (cards.length = 3): uno por dato, la ley de la línea de producción. Y cards[0] es un elemento de verdad —su tag es 'article', su props es {"className":"product-card"}—: .map() convirtió el dato del primer producto en el elemento de su tarjeta. Esto es lo que a veces cuesta ver: .map() no "pinta" nada; produce una lista de descripciones (un array de vnodes). Pintar es otro paso.
Parte 2: el array, renderizado. Ese array de tres tarjetas, puesto dentro del <section> y pasado por renderToString, produjo el HTML anidado: una <section> con tres <article>, en el mismo orden que el array de productos. React hace exactamente esto: toma el array que devolvió .map(), lo aplana dentro del contenedor, y pinta cada elemento en orden. El orden del HTML es el orden del array; el segundo producto (Mechanical Keyboard) salió segundo, con su "Out of stock".
Y nota, como en el módulo 1, la potencia: ProductList no está atado a tres. Cámbiale el array por uno de diez, y salen diez tarjetas; por uno vacío, y sale una <section> sin hijos (por eso la lección 4 le puso el early return del "sin resultados"). Una línea de .map() describe "cualquier cantidad de tarjetas".
Profundización: por qué .map() y no .forEach()
Un tropiezo frecuente de quien viene de otros estilos: intentar renderizar una lista con .forEach() o con un for. No funciona, y la razón es la tesis del módulo. Recuerda: dentro de las llaves del JSX va una expresión —algo que produce un valor—. .map() devuelve un array (un valor); por eso cabe en las llaves. .forEach(), en cambio, no devuelve nada (undefined): ejecuta una función por cada elemento pero no produce un array; sirve para efectos (imprimir, acumular en una variable externa), no para producir una colección. Un for es una sentencia, y ya sabes que no cabe en el JSX.
{/* SI: map devuelve un array de elementos */}
{props.products.map((p) => <ProductCard key={p.id} name={p.name} />)}
{/* NO: forEach no devuelve nada; el hueco queda vacio */}
{props.products.forEach((p) => <ProductCard key={p.id} name={p.name} />)}
La regla mecánica: para renderizar una colección, usa el método que transforma y devuelve un array —.map()—. Si necesitas primero filtrar (mostrar solo los que están en stock, por ejemplo), encadenas: products.filter(p => p.inStock).map(p => <ProductCard .../>) —.filter() devuelve un array más corto, y .map() lo transforma—. (Ese filtrado, cuando depende de la búsqueda del usuario, es el estado derivado del módulo 5; aquí solo el .map() sobre datos dados.)
Profundización: la key, presentada (la lección 6 la explica)
Ya la viste en el JSX: <ProductCard key={product.id} ... />. Cuando renderizas una lista de elementos con .map(), React pide que cada uno lleve una prop especial, key, con un valor único entre los hermanos. En nuestra versión de Node la omitimos —con todo estático, no cambia la salida, por eso no aparece en el HTML—, pero en React real es obligatoria: si la olvidas, React imprime una advertencia en la consola.
¿Para qué la quiere React? Para identificar cada elemento de la lista entre un render y el siguiente. React no re-dibuja todo desde cero cada vez; compara la lista de antes con la de ahora y actualiza solo lo que cambió. Para saber "cuál era cuál", empareja los elementos por su key. Si cada producto tiene una key estable (su id), React puede seguir a cada tarjeta aunque la lista se reordene o se inserte una nueva.
Aquí viene el adelanto que la lección 6 desarrolla: la key debe ser única y estable, ligada a la identidad del dato (el id del producto), no a su posición en la lista (el índice). Usar el índice parece funcionar, pero se rompe en cuanto reordenas o insertas —y la lección 6 lo demuestra ejecutando cómo el estado de un elemento "salta" al equivocado—. Por ahora, la regla práctica: pon siempre una key, y que sea el id del dato, nunca el índice.
Errores comunes
Usar .forEach() o un for para renderizar. Qué pasa: {products.forEach(p => <Card/>)} deja el hueco vacío —no aparece nada—. Por qué pasa: .forEach() parece que "recorre y hace", pero no devuelve el array; y el JSX necesita un valor. Cómo detectarlo: la lista no se renderiza y no hay error, solo vacío. Cómo corregirlo: usa .map(), que transforma y devuelve un array de elementos. Recuerda la regla: en el JSX va una expresión que produce un valor; .map() produce el array, .forEach() no.
Olvidar la key. Qué pasa: se hace products.map(p => <ProductCard ... />) sin key. Por qué pasa: la lista se ve bien a primera vista, así que es fácil no notarlo. Cómo detectarlo: React imprime "Warning: Each child in a list should have a unique key prop" en la consola; y en apps con estado (módulos siguientes) aparecen bugs raros al reordenar o filtrar. Cómo corregirlo: dale a cada elemento una key única y estable —el id del dato—. Acostúmbrate a escribirla en el mismo momento que el .map(), no "después". La lección 6 explica por qué importa tanto.
Poner la key en el elemento equivocado. Qué pasa: la key se pone en un elemento dentro del componente de la lista, en vez de en el elemento raíz que devuelve el .map(). Por ejemplo, <ProductCard> internamente pone key en su <h3>, pero el .map() que lo genera no le pasa key al <ProductCard>. Por qué pasa: se confunde "dónde vive la lista" con "dónde está el contenido". Cómo detectarlo: la advertencia de la key sigue apareciendo aunque "ya la pusiste". Cómo corregirlo: la key va en el elemento que el .map() devuelve directamente —el que está en el nivel de la lista—, no en sus hijos. En products.map(p => <ProductCard key={p.id} ... />), la key va en el <ProductCard>, que es lo que .map() produce.
Ejercicios
Ejercicio 1 — Escribe el .map(). Escribe (en JSX) un componente CartItemList que reciba props.items (un array de { id, name, quantity }) y renderice un <li> por cada item con el texto nombre xcantidad (por ejemplo, Wireless Mouse x2), todos dentro de un <ul className="cart-items">. No olvides la key.
Ver solución
function CartItemList(props) {
return (
<ul className="cart-items">
{props.items.map((item) => (
<li key={item.id} className="cart-item">
{item.name} x{item.quantity}
</li>
))}
</ul>
);
}
props.items.map(...) devuelve un array de <li>, uno por item, que React pinta dentro del <ul>. La key={item.id} va en el <li> —el elemento que el .map() devuelve directamente—, y usa el id del item (estable), no su índice. El texto combina dos expresiones en el hueco: {item.name} x{item.quantity} produce, por ejemplo, Wireless Mouse x2.
Ejercicio 2 — Predice la salida. Dado este código, ¿qué imprime cada console.log?
const products = [
{ id: 'a', name: 'Cable', priceCents: 999 },
{ id: 'b', name: 'Case', priceCents: 1999 },
];
const result = products.map((p) => p.name);
console.log(result.length);
console.log(result);
Ver solución
2
[ 'Cable', 'Case' ]
.map() devuelve un array nuevo del mismo largo que el original (2), donde cada elemento es el resultado de aplicar la función. Aquí la función es p => p.name, así que cada producto se transforma en su nombre: ['Cable', 'Case']. Es la misma mecánica que renderizar una lista, solo que en vez de transformar cada dato en un elemento (ProductCard), lo transformamos en un string (su nombre). .map() siempre devuelve un array de la misma longitud, un resultado por elemento de entrada.
Ejercicio 3 — Filtrar y luego mapear. Se quiere renderizar solo los productos en stock. Escribe la expresión (dentro de un <section>) que primero filtra props.products por inStock y luego mapea cada uno a un <ProductCard />. Explica qué devuelve cada método de la cadena.
Ver solución
<section className="product-list">
{props.products
.filter((p) => p.inStock)
.map((p) => (
<ProductCard key={p.id} name={p.name} priceCents={p.priceCents} inStock={p.inStock} />
))}
</section>
.filter((p) => p.inStock)devuelve un array más corto: solo los productos cuyoinStockestrue. No transforma los elementos, solo los selecciona..map((p) => <ProductCard .../>)toma ese array filtrado y devuelve un array de elementos, uno por producto que pasó el filtro.
La cadena .filter().map() es idiomática: primero seleccionas los datos, luego los conviertes en UI. Ojo con la frontera: este filtro es fijo (siempre inStock). Cuando el filtro dependa de lo que el usuario escribe en la búsqueda —query—, eso es estado derivado, y se computa igual con .filter().map(), pero el cuándo y el desde qué estado es el módulo 5. Aquí, el patrón; allá, conectarlo al estado.
Resumen y siguiente paso
En esta lección dominaste el patrón que más usarás en React: renderizar una lista con .map(). Entendiste que .map() es un método de los arrays de JavaScript que devuelve otro array —por eso cabe en las llaves del JSX (es una expresión) y por eso no sirve .forEach(), que no devuelve nada—: le das un array de datos y una función que convierte cada dato en un elemento, y te devuelve un array de elementos, uno por dato. Lo mediste en dos pasos: primero viste que products.map(p => ProductCard(p)) produce un array de tres vnodes (uno por producto), y luego que ese array, dentro de un <section>, se renderiza como tres <article> en orden. Y conociste la key como requisito de toda lista —única, estable, el id del dato—, con la promesa de entender su porqué en la siguiente lección.
Antes de avanzar deberías poder: renderizar una colección con .map(); explicar por qué .map() funciona en el JSX y .forEach() no; y encadenar .filter().map() para seleccionar y luego transformar.
La lección 6 es la que separa a quien copió el patrón de quien lo entiende: la key a fondo. Vas a ver, ejecutado, qué usa React la key para hacer —emparejar los elementos entre renders y conservar su estado local— y qué exactamente se rompe cuando usas el índice como key: modelamos la reconciliación de React y verás cómo, al insertar un producto al frente, el estado de un checkbox marcado "salta" al elemento equivocado. Después de esa lección, "usa el id, no el índice" deja de ser un mandamiento y pasa a ser algo que puedes demostrar.
Recursos
- React, "Rendering Lists" — react.dev/learn/rendering-lists. La página oficial sobre
.map(),.filter()y lakey; el centro de esta lección y la puerta a la 6. En inglés. - MDN, "Array.prototype.map()" — developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map. La referencia de
.map(): qué recibe, qué devuelve (un array nuevo de la misma longitud). En inglés. - MDN, "Array.prototype.filter()" — developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter. Para la cadena
.filter().map(): cómo seleccionar antes de transformar. En inglés. - React, "Your First Component" — react.dev/learn/your-first-component. Repaso de cómo un componente compone otros; el
ProductListque renderizaProductCard. En inglés.