Módulo 5: Derived State And Lists

La decisión: estado, derivado y cuándo memoizar

Descripción

Esta lección cierra la parte conceptual del módulo convirtiéndolo todo en un procedimiento que puedas aplicar sin pensarlo cada vez que diseñes estado, y desmontando los dos anti-patrones que la gente usa para "evitar" derivar —y que en realidad empeoran las cosas—. El primer anti-patrón es guardar el derivado y sincronizarlo con un useEffect: verás, ejecutado, que ese effect llega tarde —el render queda stale y hace falta uno extra—, mientras derivar es correcto en el mismo render. El segundo es "optimizar" con useMemo antes de medir: verás, cuantificado, que para listas pequeñas el ahorro es de microsegundos y no justifica la complejidad —y sabrás dónde queda la frontera con la guía de performance—.

La lección junta las dos cosas porque son las dos preguntas que aparecen apenas interiorizas "derivar, no guardar": "¿y si uso un effect para mantener la copia al día?" (respuesta: no, llega tarde) y "¿y si derivar en cada render es lento?" (respuesta: mídelo; casi nunca lo es). Contestarlas bien es lo que te deja aplicar el módulo con confianza en código real, sin caer en las dos trampas que lo rodean.

Conexión con el módulo. Es la lección de síntesis y frontera. Recoge el procedimiento de decisión de la lección 2, lo aplica a todo el storefront, y lo protege de los dos desvíos más comunes. El anti-patrón del effect es un adelanto directo del módulo 6 ("You Might Not Need an Effect"): aquí ves por qué no se usa un effect para derivar; allá verás para qué sirven los effects (hablar con sistemas externos). Y useMemo marca la frontera con la guía de performance: lo mencionamos, lo medimos, y lo remitimos —aquí se deriva en el render, a secas—.

Una analogía: el termómetro y la nota adhesiva

Imagina que quieres saber la temperatura de tu casa en cualquier momento. Tienes dos opciones. La primera: un termómetro en la pared, que siempre muestra la temperatura actual —lo miras y ahí está, correcta, sin retraso—. La segunda: una nota adhesiva donde apuntas la temperatura, y para mantenerla al día pones una alarma que, cada cierto tiempo, te recuerda ir a mirar el termómetro y reescribir la nota.

La nota adhesiva con alarma es el useEffect que sincroniza. Piénsalo: para que la nota sea correcta, primero tiene que cambiar la temperatura, después sonar la alarma, después tú vas y reescribes la nota. Entre que la temperatura cambia y la nota se actualiza, hay un rato en que la nota miente —muestra la temperatura de antes—. Y encima haces trabajo de más: mirar el termómetro y reescribir la nota, cuando el termómetro ya tenía la respuesta.

El termómetro es derivar en el render. No apuntas nada: cuando quieres la temperatura, la lees directo de la fuente, y siempre está actualizada, sin alarma, sin retraso, sin nota que mantener. El useEffect que sincroniza un estado derivado es fabricarte una nota adhesiva cuando ya tenías un termómetro en la pared: más trabajo, y con un retraso durante el cual la nota está mal. Derivar es leer el termómetro.

Parte 1: el procedimiento de decisión

Reúne todo el módulo en un solo diagrama que puedas aplicar a cualquier dato:

        ┌─────────────────────────────────────────────┐
        │  Tienes un dato que la UI necesita.          │
        │  ¿Debería ir en useState?                    │
        └───────────────────────┬─────────────────────┘
                                │
             ┌──────────────────┴──────────────────┐
             ▼                                      ▼
   ¿Se puede CALCULAR de otro                ¿Cambia por sí mismo
   estado/props que ya tienes?               (lo mueve el usuario o
             │                                un sistema) y no se
             │ sí                             deduce de nada?
             ▼                                      │ sí
      NO es estado.                                 ▼
      DERÍVALO en el render.                   ES estado.
      const x = ...                            useState(inicial)
             │
             │  ¿el cálculo es CARO y se repite
             │  con las mismas entradas?
             ▼
        ¿lo mediste?  ── no ──▶ déjalo derivado a secas (casi siempre)
             │ sí (y de verdad pesa)
             ▼
        useMemo  →  guía de performance

La rama principal es la de la lección 2: ¿se puede calcular? Si sí, deriva; si no, es estado. La rama de abajo —useMemo— solo se activa si el cálculo es caro y se repite y lo mediste, y aun así remite a la guía de performance. Por defecto, todo lo derivado se deriva en el render, sin memoizar.

Aplicado al storefront entero, la decisión ya está hecha: estado son products (los datos fuente), query, category, sort (los criterios del usuario) y cart (lo que el usuario arma). Derivado es todo lo demás —visible, el conteo, el total, hasResults, "¿está vacío?", "¿deshabilitar Checkout?"—. Cinco datos de estado; el resto, consecuencias.

Parte 2: por qué un useEffect para sincronizar llega tarde

El anti-patrón se ve así, y es tentador porque "parece" que mantiene la copia al día:

// ❌ ANTI-PATRÓN: guardar el derivado y sincronizarlo con un effect
function ProductList({ products }) {
  const [query, setQuery] = useState('');
  const [filtered, setFiltered] = useState(products);

  useEffect(() => {
    setFiltered(products.filter((p) => p.name.includes(query)));
  }, [products, query]); // "cuando cambien products o query, re-sincroniza"

  return <ul>{filtered.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}

El problema es el orden de ejecución. Un useEffect corre después de que el render terminó y se pintó. Así que cuando cambia query, la secuencia es: (1) render con el filtered viejo —la pantalla muestra la lista stale—; (2) después, el effect corre y llama a setFiltered; (3) eso dispara otro render, ahora con el filtered correcto. El usuario ve, por un instante, la lista vieja, y React hizo dos renders donde uno bastaba. Derivar, en cambio, calcula filtered durante el render, así que el primer y único render ya es correcto. Vamos a medirlo.

Parte 3: el costo de derivar (y por qué useMemo casi nunca hace falta)

La otra objeción a derivar es el miedo al costo: "si filtro en cada render, ¿no es lento?". La respuesta honesta es: mídelo. useMemo memoiza un cálculo —lo recuerda y solo lo rehace cuando sus entradas cambian—, así que evita recalcular cuando el componente re-renderiza por otra razón. Pero eso solo importa si el cálculo es caro. Vamos a contar cuántas veces corre el filtro con y sin memo a lo largo de una sesión, y a ponerlo en perspectiva.

Ejemplo trabajado: el effect tardío y el costo, medidos

Ejecutamos las dos partes. Primero, derivar (un render, correcto) frente a sincronizar-con-effect (dos renders, el primero stale) al cambiar el query. Después, contamos las llamadas al filtro con y sin memo sobre una secuencia de renders.

'use strict';
// La decisión (estado o derivado), el anti-patrón del effect que sincroniza,
// y una medición del costo (por qué useMemo casi nunca hace falta aquí).

function filterByQuery(products, query) {
  const q = query.trim().toLowerCase();
  return products.filter((p) => p.name.toLowerCase().includes(q));
}
const products = [
  { id: 'p1', name: 'Wireless Mouse',      priceCents: 2599 },
  { id: 'p2', name: 'Mechanical Keyboard', priceCents: 8900 },
  { id: 'p3', name: 'USB-C Hub',           priceCents: 3499 },
  { id: 'p4', name: 'Laptop Stand',        priceCents: 4500 },
  { id: 'p5', name: 'Desk Lamp',           priceCents: 1999 },
];

// ── PARTE A: sincronizar con un effect llega TARDE ─────────────────────────
// El patrón malo: guardar 'filtered' en estado y actualizarlo con un effect que
// corre DESPUÉS del render. Resultado: el primer render tras cambiar el query
// muestra la lista VIEJA (stale) y hace falta un segundo render para corregir.

console.log('=== A) derivar vs sincronizar-con-effect, al cambiar query "" -> "e" ===\n');

// DERIVAR: un solo render, correcto de inmediato.
let deriveRenders = 0;
function renderDerive(query) {
  deriveRenders++;
  const visible = filterByQuery(products, query); // se computa en el render
  console.log(`  derivar  render #${deriveRenders}: query="${query}" -> ${visible.length} visibles`);
}
renderDerive('e');

// EFFECT: el render lee 'filtered' (estado viejo); el effect lo actualiza DESPUÉS
// y dispara OTRO render. El primero queda stale.
let effectRenders = 0;
let filteredState = filterByQuery(products, ''); // quedó de cuando query era ""
function renderEffect(query) {
  effectRenders++;
  console.log(`  effect   render #${effectRenders}: query="${query}" -> ${filteredState.length} visibles` +
    (effectRenders === 1 ? '  <- STALE (muestra el filtro viejo)' : '  <- ya corregido'));
  // "useEffect": corre después del render; si el query cambió, re-sincroniza.
  const next = filterByQuery(products, query);
  if (next.length !== filteredState.length) {
    filteredState = next;
    renderEffect(query); // setFilteredState dispara un render extra
  }
}
console.log('');
renderEffect('e');

console.log(`\n  renders para mostrar el filtro correcto:  derivar = ${deriveRenders}   effect = ${effectRenders}`);

// ── PARTE B: el costo real de derivar (por qué useMemo casi nunca hace falta) ─
// Contamos cuántas veces corre el filtro a lo largo de una secuencia de renders.
// Sin memo: corre en CADA render. Con memo: solo cuando el query cambia.

console.log('\n=== B) costo: llamadas al filtro en 6 renders (query cambia a veces) ===\n');
const querySequence = ['e', 'e', 'a', 'a', 'a', ''];

let noMemoCalls = 0;
for (const q of querySequence) {
  noMemoCalls++; // sin memo: filtra siempre
  filterByQuery(products, q);
}

let memoCalls = 0;
let lastQuery = null;
let cached = null;
for (const q of querySequence) {
  if (q !== lastQuery) {       // con memo: recomputa solo si el query cambió
    memoCalls++;
    cached = filterByQuery(products, q);
    lastQuery = q;
  }
}

console.log('  secuencia de query:', JSON.stringify(querySequence));
console.log('  llamadas al filtro SIN memo:', noMemoCalls);
console.log('  llamadas al filtro CON memo:', memoCalls);
console.log('  (para 5 productos, la diferencia es de microsegundos: no vale la pena aún)');

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

=== A) derivar vs sincronizar-con-effect, al cambiar query "" -> "e" ===

  derivar  render #1: query="e" -> 3 visibles

  effect   render #1: query="e" -> 5 visibles  <- STALE (muestra el filtro viejo)
  effect   render #2: query="e" -> 3 visibles  <- ya corregido

  renders para mostrar el filtro correcto:  derivar = 1   effect = 2

=== B) costo: llamadas al filtro en 6 renders (query cambia a veces) ===

  secuencia de query: ["e","e","a","a","a",""]
  llamadas al filtro SIN memo: 6
  llamadas al filtro CON memo: 3
  (para 5 productos, la diferencia es de microsegundos: no vale la pena aún)

Lee las dos partes.

Parte A — el effect llega tarde. Al cambiar el query a "e", derivar produjo un solo render (render #1) ya correcto: 3 visibles. En cambio, sincronizar con effect produjo dos renders: el render #1 mostró 5 visibles —la lista vieja, de cuando query era ""—, marcado como STALE, porque el effect que recalcula todavía no había corrido; solo en el render #2, disparado por el setFilteredState del effect, la lista se corrigió a 3. El marcador final lo resume: derivar = 1 render correcto; effect = 2 renders, el primero mostrando datos viejos. Ese "flash" de la lista vieja es visible para el usuario y es exactamente el bug que derivar evita —porque calcula durante el render, no después—.

Parte B — el costo es despreciable. Sobre una secuencia de 6 renders donde el query a veces repite ("e","e","a","a","a",""), derivar sin memoizar llama al filtro 6 veces (una por render); con useMemo, solo 3 veces (recalcula únicamente cuando el query cambia respecto al render anterior). Sí, memoizar ahorra llamadas —de 6 a 3—. Pero cada llamada filtra 5 productos: son unos pocos microsegundos. Ahorrar la mitad de "casi nada" sigue siendo "casi nada". Y useMemo no es gratis: agrega complejidad, un array de dependencias que mantener, y su propio costo de comparación. Para esta lista, no vale la pena: derivar a secas es más simple y suficientemente rápido.

¿Cuándo valdría la pena? Cuando el cálculo es genuinamente caro —miles de elementos, una transformación pesada— y el componente re-renderiza seguido por otras razones y lo mediste y confirmaste que pesa. Ese análisis —cómo medir, cómo usar useMemo y useCallback, cuándo React.memo— es la guía de performance. Aquí la regla es: deriva en el render; memoiza solo cuando midas que hace falta, que con listas normales es casi nunca.

Errores comunes

Usar un useEffect para "derivar" (sincronizar estado con estado). Qué pasa: useEffect(() => setFiltered(...), [query, products]). Por qué pasa: el effect "parece" mantener la copia al día automáticamente. Cómo detectarlo: la UI parpadea con datos viejos por un instante, React hace un render de más, y tienes un estado que solo existe para copiar otro —justo lo que mediste—. Cómo corregirlo: elimina el estado y el effect; deriva en el render (const filtered = ...). La documentación de React es tajante: si puedes calcular algo durante el render, no necesitas un effect ni estado para ello. Este es el error central de "You Might Not Need an Effect" (módulo 6).

Alcanzar useMemo por reflejo, sin medir. Qué pasa: se envuelve toda derivación en useMemo "por si acaso es lenta". Por qué pasa: se asume que recalcular en cada render es caro. Cómo detectarlo: tu código está lleno de useMemo y arrays de dependencias que mantener, sin que nadie haya medido un problema de rendimiento. Cómo corregirlo: deriva a secas primero. useMemo es una optimización que se aplica después de medir un cuello de botella real; aplicarla antes agrega complejidad sin beneficio comprobado (y un useMemo mal escrito, con dependencias incorrectas, introduce bugs de datos viejos). La regla: primero correcto y simple (derivar), luego rápido si hace falta (memoizar, en la guía de performance).

Confundir "derivar es caro" con "guardar es más rápido". Qué pasa: se guarda el derivado "para no recalcular". Por qué pasa: parece que guardar evita trabajo. Cómo detectarlo: cambiaste un bug de rendimiento inexistente por un bug de desincronización real (lección 4). Cómo corregirlo: guardar no es "más rápido"; es incorrecto (se desincroniza) y encima, si lo sincronizas con un effect, es más lento (un render extra). Derivar es a la vez lo correcto y, para listas normales, lo suficientemente rápido. No cambies corrección por una optimización que no mediste.

Ejercicios

Ejercicio 1 — Aplica el procedimiento. Para cada dato, recorre el diagrama de decisión y di el resultado (estado / derivado / derivado-y-quizás-memo), con una frase de justificación: (a) el sort elegido en el menú; (b) la lista visible getVisibleProducts(...); (c) el resultado de un cálculo que cruza 50.000 filas y corre en cada tecleo; (d) si el carrito está vacío.

Ver solución
  • (a) sort — ESTADO. Cambia por acción del usuario (elige en el menú) y no se deduce de nada. useState('price-asc').
  • (b) La lista visible — DERIVADO (a secas). Se calcula de products, query, category, sort. Se deriva en el render. Con 5 (o unos miles de) productos, sin memo: barato.
  • (c) El cálculo sobre 50.000 filas por tecleo — DERIVADO, y aquí sí candidato a memo. Se calcula de otros datos (es derivado), pero es caro y se repite en cada tecleo. Si al medir confirmas que pesa, useMemo con las entradas como dependencias evita recalcular cuando el componente re-renderiza por otra razón. Esto es territorio de la guía de performance.
  • (d) Carrito vacío — DERIVADO. cart.length === 0. Un booleano calculado en el render. Trivial, jamás memo.

Solo (a) es estado. (b) y (d) se derivan a secas; solo (c), tras medir, justificaría useMemo.

Ejercicio 2 — Explica el flash. Un compañero usa el patrón del useEffect que sincroniza filtered, y reporta que "al escribir en la búsqueda, por un instante se ve la lista sin filtrar y luego se filtra". Explica, con la salida de la Parte A, por qué pasa eso y cómo lo elimina derivar.

Ver solución

El "flash" es el render #1 stale que viste en la Parte A: cuando el query cambia, el componente re-renderiza inmediatamente con el filtered que todavía tiene el valor viejo (en la salida, 5 visibles, la lista sin filtrar), porque el useEffect que recalcula corre después de pintar. Solo cuando el effect corre y llama a setFiltered, un segundo render (render #2) muestra la lista correcta (3 visibles). Entre esos dos renders, el usuario ve la lista vieja: ese es el flash.

Derivar lo elimina porque calcula filtered durante el render, no después: el primer (y único) render ya usa el query nuevo, así que muestra directamente los 3 visibles correctos —derivar = 1 render, sin flash—. El effect es la nota adhesiva que se actualiza tarde; derivar es el termómetro que siempre está al día.

Ejercicio 3 — ¿Memoizar o no? Para cada situación, decide si derivar a secas basta o si conviene medir con miras a useMemo, y por qué: (a) filtrar 20 productos por nombre en cada tecleo; (b) calcular el total del carrito (una suma) en cada render; (c) un ordenamiento de 100.000 elementos con una comparación compleja, en un componente que re-renderiza cada segundo por un reloj; (d) formatear un precio con formatPrice.

Ver solución
  • (a) Filtrar 20 productos — derivar a secas. 20 elementos es nada; filtrar cuesta microsegundos. Memoizar agregaría complejidad sin beneficio.
  • (b) Total del carrito (una suma) — derivar a secas. Sumar unos pocos números es instantáneo. Jamás memo.
  • (c) Ordenar 100.000 con comparación compleja, en un componente que re-renderiza cada segundo — candidato a medir y quizás useMemo. Aquí sí: el cálculo es caro y el componente re-renderiza seguido por una razón ajena (el reloj), así que recalcular el orden en cada tick sería desperdicio. Mide primero; si pesa, useMemo con las entradas del ordenamiento como dependencias evita rehacerlo cuando solo cambió el reloj. Territorio de la guía de performance.
  • (d) Formatear un precio — derivar a secas. Una operación de strings trivial. Nunca memo.

La regla que los separa: memoizar solo se plantea cuando el cálculo es caro y el componente re-renderiza seguido por otras razones y lo mediste. Solo (c) cumple los tres. Los demás se derivan a secas.

Resumen y siguiente paso

En esta lección convertiste el módulo en un procedimiento —¿se puede calcular de lo que ya tienes? sí → deriva; no → estado; y solo memoiza si es caro, se repite, y lo mediste— y lo protegiste de sus dos anti-patrones. Mediste que sincronizar con un useEffect llega tarde: al cambiar el query, el effect produjo dos renders con el primero mostrando la lista vieja (stale), mientras derivar produjo un solo render ya correcto. Y mediste que useMemo casi nunca hace falta con listas pequeñas: memoizar bajó las llamadas al filtro de 6 a 3, pero filtrar 5 productos cuesta microsegundos, así que el ahorro no justifica la complejidad. La regla queda clara: deriva en el render por defecto; memoiza solo cuando midas un problema real, que es tema de la guía de performance.

Antes de avanzar deberías poder: recorrer el procedimiento de decisión sobre cualquier dato; explicar por qué un effect que sincroniza estado derivado llega tarde y sobra; y decir cuándo (raramente) useMemo se justifica y dónde queda la frontera.

La lección 8 es el mini-proyecto: construyes el ProductList de Mercado de punta a punta, aplicando todo el módulo a la vez —el query, la category y el sort como único estado fuente, la lista visible derivada con getVisibleProducts, y key = id estable—. El entregable son el árbol de componentes, el código React real, y la lógica ejecutada en Node: una sesión completa (teclear, filtrar por categoría, reordenar), la comprobación de que el derivado no se desincroniza cuando cambian los productos, y la key estable conservando el estado local al reordenar. Ahí, "derivar, no guardar" pasa de conjunto de reglas a una pieza de software que funciona.

Recursos