Módulo 5: Derived State And Lists
Derivar en el cuerpo del componente
Descripción
En la lección 2 decidiste qué es derivado. Esta lección enseña cómo se deriva, y la respuesta es tan simple que sorprende: escribes una línea de JavaScript normal —const visible = products.filter(...).sort(...)— en el cuerpo del componente, y ya está. No hay hook especial, no hay API de React, no hay nada que "activar". Derivar es solo calcular una variable local en el render a partir del estado y las props que tienes a mano. React vuelve a ejecutar el cuerpo del componente en cada render, así que esa variable se recalcula fresca cada vez, automáticamente.
La lección desmonta, de paso, dos miedos que frenan a la gente. El primero: "si lo calculo en el cuerpo, se recalcula en cada render, ¿no es un desperdicio?". Para listas normales, no —lo mediremos en la lección 7—; y de todos modos es lo correcto por defecto. El segundo, más técnico y más importante: "si hago .sort(), ¿no estoy mutando el array de productos?". La respuesta —que verás ejecutada— es que .filter() devuelve un array nuevo, así que cuando encadenas .filter().sort(), el .sort() opera sobre esa copia fresca, no sobre products. La fuente queda intacta. (El peligro real es products.sort() a secas, sin filter delante: eso sí mutaría la fuente, y lo veremos.)
Conexión con el módulo. Esta es la lección de la mecánica. La 1 dio la idea (derivar, no guardar), la 2 dio la decisión (qué es derivado); esta da el cómo: la línea que escribes en el cuerpo, dónde va, qué devuelve, y por qué no muta nada. La lección 4 mostrará el precio de no hacerlo así (guardar y desincronizarse), y la 5 llevará este mismo patrón al ProductList completo con categoría. Aquí trabajamos la forma canónica —getVisibleProducts(products, query, sort)— y la ejecutamos con varios inputs para verla producir la lista visible en el render.
Una analogía: la calculadora, no la libreta
Imagina dos formas de llevar la cuenta de cuánto llevas gastado en un viaje. La primera: una libreta donde anotas un número "total gastado", y cada vez que compras algo, tachas el número viejo y escribes el nuevo. La segunda: guardas todos los tickets en un sobre, y cuando quieres saber el total, sacas la calculadora y los sumas en el momento.
La libreta es "guardar el derivado". Funciona mientras seas perfecto: si una vez compras algo y no actualizas la libreta, el número miente para siempre. La calculadora es "derivar en el render": no guardas ningún total; guardas los tickets (el dato de verdad) y calculas el total cada vez que lo necesitas. Es imposible que el total esté desactualizado, porque no existe hasta que lo pides —y cuando lo pides, se hace fresco sobre los tickets actuales—.
Derivar en el cuerpo del componente es sacar la calculadora en cada render. React te da los tickets (el estado, las props) y tú, en el cuerpo, sumas: const visible = products.filter(...). No guardas el resultado en ninguna "libreta" (ningún useState); lo calculas ahí mismo, lo usas en el JSX de ese render, y en el siguiente render lo vuelves a calcular. Sale gratis en simplicidad lo que la libreta cobra en bugs.
La mecánica: una variable en el cuerpo
Un componente de React es una función que corre de arriba a abajo en cada render. Derivar es, simplemente, declarar una variable en medio de esa función, entre los hooks y el return:
function ProductList({ products, query, sort }) {
// 1) los hooks van primero (aquí no hay; el estado vive en el padre)
// 2) DERIVAR: variables locales calculadas del estado/props de este render
const q = query.trim().toLowerCase();
const visible = products
.filter((p) => p.name.toLowerCase().includes(q))
.sort((a, b) =>
sort === 'price-desc' ? b.priceCents - a.priceCents : a.priceCents - b.priceCents
);
// 3) el JSX usa la variable derivada
return (
<ul className="product-list">
{visible.map((p) => (
<ProductCard key={p.id} name={p.name} priceCents={p.priceCents} />
))}
</ul>
);
}
visible es una variable local, como cualquier otra en una función. Vive solo durante ese render: se crea al principio, se usa en el return, y se desecha cuando el render termina. En el siguiente render, la función corre otra vez y visible se crea de nuevo, con los valores de query y sort de ese momento. Por eso siempre está actualizada: no persiste, se recrea.
Nota que no importó cómo se calcula —una línea o veinte, un .filter() o un algoritmo complejo—: mientras sea una función del estado y las props que ya tienes, va en el cuerpo, no en un useState. Cuando el cálculo se repite en varios componentes o crece, es buena idea extraerlo a una función pura con nombre, y pasarle los datos como argumentos:
function getVisibleProducts(products, query, sort) {
const q = query.trim().toLowerCase();
return products
.filter((p) => p.name.toLowerCase().includes(q))
.sort((a, b) =>
sort === 'price-desc' ? b.priceCents - a.priceCents : a.priceCents - b.priceCents
);
}
function ProductList({ products, query, sort }) {
const visible = getVisibleProducts(products, query, sort); // derivado, una línea
// ...
}
Esta función pura es la que ejecutaremos. "Pura" significa que solo depende de sus argumentos y no toca nada de afuera: mismos argumentos, mismo resultado, sin efectos secundarios. Eso la hace trivial de correr en Node —le pasas datos fijos y verificas la salida— y trivial de razonar.
.filter() copia: por qué .sort() no muta la fuente
Aquí hay una sutileza que atrapa a mucha gente, y vale la pena entenderla bien porque es la diferencia entre código correcto y un bug silencioso.
.sort() es un método mutante: ordena el array en su lugar y devuelve ese mismo array. Si escribieras products.sort(...) directamente, estarías reordenando el array products original —el estado— por dentro, lo cual es exactamente lo que el módulo 3 te enseñó a nunca hacer (no mutar el estado). Ese sería un bug feo: derivar no debe tocar la fuente.
Pero mira la cadena products.filter(...).sort(...). .filter() no muta: crea y devuelve un array nuevo con los elementos que pasan el test. Ese array nuevo es una copia fresca, independiente de products. Cuando encadenas .sort() después del .filter(), el .sort() opera sobre esa copia, no sobre products. Reordena la copia; la fuente ni se entera. Por eso products.filter(...).sort(...) es seguro: el .filter() de adelante ya hizo la copia que el .sort() va a mutar.
La regla práctica: si vas a ordenar, asegúrate de que haya una copia antes. .filter() la provee gratis. Si por alguna razón no filtras y solo quieres ordenar, haz la copia tú: [...products].sort(...) o products.slice().sort(...). Lo que nunca debes escribir es products.sort(...) a secas sobre el estado. Vamos a verlo ejecutado: derivamos tres veces y comprobamos que products sigue en su orden original.
Ejemplo trabajado: derivar sin tocar la fuente
Ejecutamos getVisibleProducts(products, query, sort) con tres combinaciones de búsqueda y orden, y después imprimimos el orden de products para probar que las tres derivaciones no lo mutaron.
'use strict';
// El patrón `const visible = products.filter(...).sort(...)` en el cuerpo.
// .filter() DEVUELVE UN ARRAY NUEVO, así que .sort() sobre ese resultado NO
// muta el array original 'products'.
function formatPrice(cents) {
return '$' + (cents / 100).toFixed(2);
}
const products = [
{ id: 'p1', name: 'Wireless Mouse', priceCents: 2599, category: 'peripherals', inStock: true },
{ id: 'p2', name: 'Mechanical Keyboard', priceCents: 8900, category: 'peripherals', inStock: false },
{ id: 'p3', name: 'USB-C Hub', priceCents: 3499, category: 'peripherals', inStock: true },
{ id: 'p4', name: 'Laptop Stand', priceCents: 4500, category: 'furniture', inStock: true },
{ id: 'p5', name: 'Desk Lamp', priceCents: 1999, category: 'furniture', inStock: true },
];
// Esto es lo que escribirías en el CUERPO del componente, en cada render:
function getVisibleProducts(products, query, sort) {
const q = query.trim().toLowerCase();
return products
.filter((p) => p.name.toLowerCase().includes(q))
.sort((a, b) =>
sort === 'price-desc' ? b.priceCents - a.priceCents : a.priceCents - b.priceCents
);
}
function show(label, list) {
console.log(label + ' -> ' + list.length + ' visibles');
for (const p of list) console.log(' - ' + p.name.padEnd(20) + formatPrice(p.priceCents));
console.log('');
}
console.log('=== derivar en el cuerpo: filter + sort, distintos inputs ===\n');
show("query='' sort=asc ", getVisibleProducts(products, '', 'price-asc'));
show("query='mouse' sort=asc", getVisibleProducts(products, 'mouse', 'price-asc'));
show("query='e' sort=desc", getVisibleProducts(products, 'e', 'price-desc'));
console.log('=== el original NO se mutó (filter copió antes de sort) ===');
console.log('orden de products tras 3 derivaciones:');
console.log(' ' + products.map((p) => p.id).join(', '));
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== derivar en el cuerpo: filter + sort, distintos inputs ===
query='' sort=asc -> 5 visibles
- Desk Lamp $19.99
- Wireless Mouse $25.99
- USB-C Hub $34.99
- Laptop Stand $45.00
- Mechanical Keyboard $89.00
query='mouse' sort=asc -> 1 visibles
- Wireless Mouse $25.99
query='e' sort=desc -> 3 visibles
- Mechanical Keyboard $89.00
- Wireless Mouse $25.99
- Desk Lamp $19.99
=== el original NO se mutó (filter copió antes de sort) ===
orden de products tras 3 derivaciones:
p1, p2, p3, p4, p5
Lee las tres derivaciones. Con query='' y orden ascendente, la lista visible son los cinco productos de barato ($19.99) a caro ($89.00). Con query='mouse', el filtro deja solo Wireless Mouse: 1 visible. Con query='e' y orden descendente, los tres productos con "e" salen de caro a barato. Cada llamada produjo una lista distinta a partir de los mismos products, sin guardar nada entre llamadas: eso es derivar en el render.
Y ahora lo importante, la última línea. Después de las tres derivaciones —dos de las cuales ordenaron, una ascendente y otra descendente—, el orden de products sigue siendo p1, p2, p3, p4, p5: exactamente el original. Ninguno de los tres .sort() tocó la fuente. ¿Por qué? Porque cada uno operó sobre el array nuevo que .filter() produjo, no sobre products. El .filter() de adelante hizo la copia; el .sort() reordenó la copia; products quedó intacto. Si en vez de products.filter(...).sort(...) hubiéramos escrito products.sort(...), esta línea final habría mostrado un orden distinto —la fuente mutada— y tendríamos un bug. Derivar bien es derivar sin tocar la fuente, y .filter() te lo regala.
Derivar no es solo para listas: valores sueltos también
Es fácil asociar "derivar" con listas —filtrar y ordenar—, pero el hábito aplica a cualquier valor que la UI muestre. Un texto, un número, un booleano, un color: si se puede calcular del estado y las props, se deriva en el cuerpo, igual que la lista. La lista es solo el caso más vistoso.
Mira un ProductCard que muestra varios valores, todos derivados de sus props:
function ProductCard({ product }) {
// Todos DERIVADOS de las props, en el cuerpo, en cada render:
const priceLabel = formatPrice(product.priceCents); // "$25.99"
const isCheap = product.priceCents < 3000; // booleano
const stockLabel = product.inStock ? 'In stock' : 'Out of stock';
const badge = isCheap ? 'Deal' : null; // qué badge mostrar
return (
<article className="product-card">
<h3>{product.name}</h3>
<p className={isCheap ? 'price cheap' : 'price'}>{priceLabel}</p>
<span>{stockLabel}</span>
{badge && <span className="badge">{badge}</span>}
</article>
);
}
Ninguno de esos cuatro valores es estado; ninguno se guarda. priceLabel es una función de priceCents; isCheap es una comparación; stockLabel y badge son consecuencias de otras propiedades. Se calculan arriba, con nombres claros, y el JSX solo los usa. Es la misma idea de la lista, aplicada a escalares: el cuerpo del componente es donde vive todo lo derivado, sea una colección o un solo valor.
Dónde va la derivación: hooks, cálculo, retorno
La derivación tiene un lugar en la estructura del componente, y conviene fijarlo porque romperlo causa bugs sutiles. El orden canónico del cuerpo es:
function Component(props) {
// 1) HOOKS primero, siempre (useState, useEffect...), sin condicionales
// 2) DERIVAR: variables calculadas del estado/props de este render
// 3) EARLY RETURNS condicionales (if vacío, if error), usando lo derivado
// 4) el JSX principal
}
Los hooks van primero (la regla de los hooks del módulo 3: nunca dentro de if ni después de un return). Luego derivas. Luego, si hace falta, un retorno temprano que use un valor derivado (por ejemplo, if (visible.length === 0) return <Empty />). Y al final el JSX. Poner la derivación después de un return la volvería inalcanzable en ese caso; ponerla antes de los hooks no cambia nada (no depende de ellos) pero rompe la lectura. La forma limpia: hooks, derivar, retornos tempranos, JSX. Así cada pieza está donde React y el lector la esperan.
Errores comunes
Ordenar la fuente directamente con products.sort(...). Qué pasa: en el cuerpo se escribe const visible = products.sort((a, b) => a.priceCents - b.priceCents) sin copiar antes. Por qué pasa: .sort() parece devolver "un array ordenado", y uno olvida que ordena en su lugar y devuelve el mismo. Cómo detectarlo: el orden de la lista se "pega" —después de ordenar una vez, la fuente queda reordenada y cambiar el criterio se comporta raro—, y si products es estado, acabas de mutarlo (prohibido, módulo 3). Cómo corregirlo: copia antes de ordenar. Si ya filtraste (products.filter(...).sort(...)), estás a salvo porque .filter() copió. Si solo ordenas, hazlo explícito: [...products].sort(...) o products.slice().sort(...).
Meter la derivación en un useState con valor inicial. Qué pasa: const [visible] = useState(products.filter(...)). Por qué pasa: se cree que "guardarlo en estado" es la forma de tenerlo disponible. Cómo detectarlo: la lista no se actualiza cuando cambian query o products, porque useState(x) solo usa x como valor inicial y luego lo ignora. Cómo corregirlo: no es estado, es derivado: una variable normal en el cuerpo (const visible = products.filter(...)), que se recalcula en cada render. El useState es para lo que cambia por sí mismo, no para lo que se computa.
Derivar dentro del .map() del JSX, mezclando cálculo y marcado. Qué pasa: en vez de calcular visible arriba, se hace products.filter(...).sort(...).map(...) todo dentro del return, con lógica compleja incrustada. Por qué pasa: "está todo junto, para qué separarlo". Cómo detectarlo: el JSX se vuelve una sopa difícil de leer, y no puedes reusar ni testear el cálculo. Cómo corregirlo: calcula la variable derivada arriba, con un nombre claro (const visible = ...), y deja el JSX limpio ({visible.map(...)}). Derivar en el cuerpo no significa derivar dentro del JSX; significa una variable con nombre antes del return. (Esto es primo del "handlers limpios" del módulo 4: separa el cálculo del marcado.)
Ejercicios
Ejercicio 1 — Escribe la derivación. En el cuerpo de un componente que recibe products por props y tiene query en estado, escribe la variable derivada inStockMatches: los productos que (a) coinciden con query en el nombre y (b) tienen inStock === true, ordenados por precio ascendente. No la guardes en estado.
Ver solución
function ProductList({ products }) {
const [query, setQuery] = useState('');
const q = query.trim().toLowerCase();
const inStockMatches = products
.filter((p) => p.name.toLowerCase().includes(q)) // coincide con la búsqueda
.filter((p) => p.inStock) // solo disponibles
.sort((a, b) => a.priceCents - b.priceCents); // por precio ascendente
return (
<ul>
{inStockMatches.map((p) => (
<ProductCard key={p.id} name={p.name} priceCents={p.priceCents} />
))}
</ul>
);
}
Tres transformaciones encadenadas, todas en el cuerpo, ninguna en estado. El primer .filter() ya copia, así que el .sort() del final no muta products. La variable inStockMatches se recalcula en cada render con el query de ese momento.
Ejercicio 2 — ¿Muta o no muta? Para cada expresión, di si muta el array products original o no, y por qué: (a) products.filter(p => p.inStock); (b) products.sort((a,b) => a.priceCents - b.priceCents); (c) [...products].sort(...); (d) products.filter(...).sort(...); (e) products.map(p => p.name).
Ver solución
- (a)
products.filter(...)— NO muta..filter()crea y devuelve un array nuevo; deja el original intacto. - (b)
products.sort(...)— SÍ muta..sort()ordena en su lugar; reordenaproductspor dentro y devuelve el mismo array. Peligroso sobre el estado. - (c)
[...products].sort(...)— NO muta.[...products]hace una copia primero; el.sort()opera sobre la copia. El original queda intacto. - (d)
products.filter(...).sort(...)— NO muta. El.filter()devuelve un array nuevo; el.sort()reordena ese array nuevo, noproducts. Seguro. - (e)
products.map(...)— NO muta..map()crea y devuelve un array nuevo con los resultados; no toca el original.
La regla: .filter(), .map(), .slice(), el spread [...] copian (seguros); .sort() y .reverse() mutan (necesitan una copia antes). En una cadena, basta con que haya un método que copie antes del que muta.
Ejercicio 3 — Predice y explica. Sin correr nada, predice la salida de show("query='a' sort=asc", getVisibleProducts(products, 'a', 'price-asc')) usando los datos del ejemplo trabajado. Pista: revisa qué nombres contienen la letra "a" en minúsculas.
Ver solución
Nombres en minúsculas: wireless mouse (no tiene "a"), mechanical keyboard (sí: mechanical), usb-c hub (no), laptop stand (sí: laptop stand), desk lamp (sí: lamp). Coinciden tres: Mechanical Keyboard (8900), Laptop Stand (4500), Desk Lamp (1999). Ordenados ascendente por precio:
query='a' sort=asc -> 3 visibles
- Desk Lamp $19.99
- Laptop Stand $45.00
- Mechanical Keyboard $89.00
Se derivaron tres productos de barato a caro. Wireless Mouse y USB-C Hub quedaron fuera porque sus nombres no contienen "a". Todo calculado en la llamada, sin guardar nada.
Resumen y siguiente paso
En esta lección aprendiste la mecánica de derivar: una variable local en el cuerpo del componente, calculada del estado y las props, que corre en cada render y se usa en el JSX —const visible = products.filter(...).sort(...), o una función pura getVisibleProducts(...) cuando el cálculo crece—. Y aprendiste la sutileza que la hace segura: .filter() devuelve un array nuevo, así que .sort() encadenado reordena esa copia y no muta la fuente. Lo comprobaste ejecutado: tres derivaciones (dos con orden) dejaron products en su orden original p1..p5, intacto. El peligro real es products.sort() a secas; con un .filter() (o un [...products]) delante, estás a salvo.
Antes de avanzar deberías poder: escribir una derivación en el cuerpo con un nombre claro; explicar por qué .filter().sort() no muta pero .sort() a secas sí; y separar el cálculo (arriba, con nombre) del marcado (el JSX limpio).
La lección 4 va al corazón del módulo y responde, midiéndolo, la pregunta que todo esto asume: ¿qué pasa exactamente si en vez de derivar, guardas? Vas a ver la lista filtrada guardada en estado quedar stale cuando cambian los productos —cobrando el precio viejo, perdiendo el producto nuevo— mientras la versión derivada está siempre correcta. La desincronización, cuantificada. Ahí entenderás por qué "derivar, no guardar" no es una preferencia de estilo, sino la única opción sin bugs.
Recursos
- React, "Keeping Components Pure" — react.dev/learn/keeping-components-pure. Por qué el cuerpo del componente debe ser una función pura del estado y las props (y por eso no debe mutar la fuente al derivar). En inglés.
- React, "Rendering Lists" — react.dev/learn/rendering-lists. Cómo transformar un array de datos en un array de JSX con
.map(), el paso final de derivar una lista. En inglés. - MDN, "Array.prototype.sort()" — developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort. La documentación que aclara que
.sort()ordena en su lugar (muta) y devuelve el mismo array; clave para no mutar la fuente. En inglés. - MDN, "Array.prototype.filter()" — developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter. El método que devuelve un array nuevo, base de la derivación segura
filter().sort(). En inglés.