Ir al contenido
wannacode

Estado derivado

En resumen: El estado derivado es un valor que se calcula a partir del estado durante el render (una cantidad, un total, una lista filtrada) en lugar de guardarse aparte; para un cálculo costoso, useMemo guarda el resultado entre renders.

Problema

El total se queda atrás

El carrito tiene pan blanco y leche fresca. Pulsa «+» en el pan: la cantidad y el total crecen. Ahora quita la leche.

React

La línea desaparece, la cantidad baja a 3, y el total se queda en 5,20 €. El componente guarda tres estados: items, count y total. Cada clic tiene que cambiar los tres, y la función de quitar cambia dos. Es la misma deriva que en la lección «Una lista a partir de datos», solo que dentro de un componente. La biblioteca dibuja exactamente lo que le das. Le das dos verdades sobre un mismo carrito.

La idea

Calcúlalo, no lo guardes

Pregunta por cada pieza de estado: ¿puedo calcularla a partir del otro estado o de las props? Si puedes, no es estado. La cantidad es la suma de las cantidades de items. El total es la suma de precio por cantidad. Las dos se calculan.

Un valor así se llama estado derivado (derived state). Guardas solo items y calculas el resto a partir de él cada vez que el componente se renderiza.

const [items, setItems] = useState(START);
const count = items.reduce((sum, i) => sum + i.qty, 0);
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);

setItems hace que React ejecute otra vez la función App. Entonces count y total se calculan a partir de los items nuevos. Son constantes normales. No necesitan useState ni nada especial.

Ahora las funciones more y remove cambian solo items. Ya no queda nada que puedan olvidar.

Míralo

Mira los valores calculados y la copia

A la izquierda están el carrito y una copia del total, guardada aparte. A la derecha están los tres valores calculados.

const [items, setItems] = useState(START);
const [savedTotal, setSavedTotal] = useState(26.9);
const count = items.reduce((s, i) => s + i.qty, 0);
const total = items.reduce((s, i) => s + i.price * i.qty, 0);
const free = total >= 30;
function more(item) {
setItems(items.map((i) =>
i.id === item.id ? { ...i, qty: i.qty + 1 } : i,
));
setSavedTotal(savedTotal + item.price);
}
function remove(item) {
setItems(items.filter((i) => i.id !== item.id));
}
Paso a paso, en palabras
  1. A la izquierda está el estado: los elementos del carrito items. Debajo hay un segundo estado, savedTotal: el mismo total, guardado como copia.
  2. A la derecha están count, total y free: constantes normales. React ejecuta App en cada render, así que se calculan de nuevo a partir de items cada vez: 26,90 €, faltan 3,10 € para el envío gratis.
  3. more(queso): items cambia y las tres flechas se encienden. El total es 34,80 €, el envío es gratis. more() también actualiza savedTotal a mano, así que la copia todavía es correcta.
  4. remove(queso): items cambia, y count, total y free lo siguen solos: 3,20 €. Para savedTotal en remove() nadie escribió una línea.
  5. La copia dice 34,80 €, el carrito es 3,20 €. Ningún error, solo un número falso: el estado y su copia se han separado.
  6. La regla: guarda solo lo que no se puede calcular. Lo que se deduce del estado se calcula a partir de él, y no hay ninguna copia que olvidar.

Los valores calculados siguen al carrito en cada cambio, porque no tienen memoria propia. La copia lo sigue solo mientras alguien se acuerde de actualizarla. La primera función que se olvide la deja mal.

Pruébalo

Calcula la cantidad y el total

El carrito guarda count y total como estados aparte, y remove() se olvida del total. Quita los dos estados y calcula la cantidad y el total a partir de items. La comprobación pulsa «+» en el pan, luego quita la leche y mira la cantidad y el total.

React

Pruébalo

Rellena el hueco

La cantidad se calcula a partir del carrito. ¿Qué va en el hueco?

const [items, setItems] = useState(START);
 count = items.reduce((sum, i) => sum + i.qty, 0);

La idea

Cuándo guardar el resultado

La constante se calcula en cada render del componente, incluso cuando items es el mismo y el render viene de otro estado. Con un total de diez líneas eso no es nada. Si el cálculo es costoso (ordenar miles de productos), React tiene useMemo:

const sorted = useMemo(() => products.toSorted(byPrice), [products]);

React guarda el resultado y ejecuta la función otra vez solo si alguna de las dependencias del array ([products]) es distinta de las del render anterior. Si olvidas una dependencia, el resultado se queda viejo. Por eso useMemo no es para cualquier total. Empieza con una constante y añádelo cuando midas que el cálculo es lento. El resultado es el mismo, solo se calcula con menos frecuencia.

La idea

El filtro también se calcula

El catálogo tiene los botones «Todos», «Pan» y «Leche». La tentación es guardar dos arrays: todos los productos y los visibles. Pero los visibles se deducen de otros dos datos: la lista completa y la categoría elegida. Guardas solo la categoría:

const [category, setCategory] = useState('all');
const visible = category === 'all' ? PRODUCTS : PRODUCTS.filter((p) => p.category === category);

Con la ordenación pasa igual: guardas «por precio» o «por nombre», y la lista ordenada se calcula. Ojo con sort(): ordena el array en el sitio, es decir, cambia el estado. toSorted() devuelve un array nuevo y deja el viejo.

Pruébalo

Solo la categoría

Los botones cambian la categoría, pero la lista siempre muestra todo. Calcula los productos visibles a partir de la categoría y muestra «Mostrados: 3 de 5» encima de la lista. La comprobación elige «Leche», luego «Pan», luego «Todos».

React

La idea

Una copia de una prop

El mismo error ocurre entre componentes. App da un 20 % de descuento y pasa los precios nuevos a cada ProductCard. La tarjeta, sin embargo, recuerda el precio que recibió la primera vez:

React

Pulsa «20 % de descuento». El botón queda pulsado, y el pan sigue costando 1,20 €.

const [price] = useState(product.price) toma product.price solo en el primer render de la tarjeta. Después React renderiza la tarjeta con el product nuevo, pero su estado es suyo y se queda.

La solución es no copiar: muestra product.price directamente. Si la tarjeta tiene que mostrar algo calculado a partir de una prop (el precio con IVA, el precio por unidad), calcúlalo durante el render.

Pruébalo

Encuentra el error

La compradora pulsa «+»: la cantidad de la línea pasa a 3, y el total de la línea se queda como con 2. ¿Qué línea tiene la culpa?

Elige la línea con el error.

Más precisamente, para curiosos

Cuando la copia es a propósito. A veces quieres un valor de fuera solo como punto de partida: un formulario de edición empieza con el nombre actual, y luego la persona escribe. Entonces la copia es correcta, pero dilo en el nombre: la prop initialName, el estado name. Quien lee el código ve que no se siguen los cambios de fuera.

useMemo es una pista. React se reserva el derecho de tirar el resultado recordado y calcular de nuevo, y la documentación lo dice explícitamente. Así que el código tiene que ser correcto sin useMemo. React Compiler, si el proyecto lo usa, recuerda por sí solo esos cálculos, y un useMemo manual hace falta aún menos.

Los renders son frecuentes. Un componente se vuelve a renderizar cuando cambia su estado o cuando se renderiza su padre. Las constantes de dentro se calculan cada vez. Por eso el cuerpo de un componente no lleva fetch, temporizadores ni cambios de estado: eso es para los efectos, el módulo siguiente.

Lo mismo vale para los datos del servidor. Si el servidor devuelve los productos y guardas «número de productos» aparte, los dos pueden separarse después de una carga nueva. Guarda la respuesta y calcula la cantidad.

Ejercicio

Envío gratis

El carrito tiene botones «−» y «+» en cada línea («−» en la última unidad quita la línea). Debajo de la lista muestra «Artículos: 4», «Total: 18,20 €» y «Para el envío gratis faltan 11,80 €», y a partir de 30 € (FREE_FROM) solo «El envío es gratis». Todo se calcula a partir de items. La comprobación añade queso hasta 34,00 €, luego vuelve por debajo de 30 € y quita el pan.

React

Test

Ponte a prueba

1. El carrito guarda items. ¿Cuál de estos debería ser un estado aparte?
2. ¿Cuándo conviene envolver un cálculo en useMemo?
3. const [price] = useState(product.price); en ProductCard. El padre pasa un precio nuevo. ¿Qué muestra la tarjeta?
4. La lista se filtra por categoría. ¿Qué va en el estado?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·