Ir al contenido
wannacode

Estado derivado

En resumen: El estado derivado es un valor que se calcula a partir del estado (una cantidad, un total, una lista filtrada) en lugar de guardarse aparte; computed guarda el resultado hasta el siguiente cambio.

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.

Vue

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 = ref(START);
const count = computed(() => items.value.reduce((sum, i) => sum + i.qty, 0));
const total = computed(() => items.value.reduce((sum, i) => sum + i.price * i.qty, 0));

<script setup> se ejecuta una vez, cuando se crea el componente. Por eso el cálculo está en una función que se le da a computed. Vue recuerda que lee items y la ejecuta otra vez cuando items cambia. En la plantilla escribes {{ total }}, sin .value.

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.

<script setup>
const items = ref(START);
const savedTotal = ref(26.9);
const count = computed(() =>
items.value.reduce((s, i) => s + i.qty, 0),
);
const total = computed(() =>
items.value.reduce((s, i) => s + i.price * i.qty, 0),
);
const free = computed(() => total.value >= 30);
function more(item) {
item.qty++;
savedTotal.value += item.price;
}
function remove(item) {
items.value = items.value.filter((i) => i.id !== item.id);
}
</script>
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: valores computed. Vue recuerda que leen items y los calcula de nuevo cuando items cambia: 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.

Vue

Pruébalo

Rellena el hueco

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

const items = ref(START);
const count = (() => items.value.reduce((sum, i) => sum + i.qty, 0));

La idea

Cuándo guardar el resultado

computed ya guarda el resultado. Se calcula cuando lo lees por primera vez. Después, mientras los datos que leyó no cambien, devuelve el valor recordado. Así que dos sitios de la plantilla con {{ total }} calculan el total una sola vez.

const sorted = computed(() => products.value.toSorted(byPrice));

Una función llamada en la plantilla ({{ totalOf(items) }}) también da el total correcto. La diferencia es que se ejecuta en cada render del componente, y computed solo cuando cambian sus dependencias. La función de computed solo lee y devuelve. No cambia el estado ni llama a fetch.

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 = ref('all');
const visible = computed(() =>
category.value === 'all' ? PRODUCTS : PRODUCTS.filter((p) => p.category === category.value),
);

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».

Vue

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:

Vue

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

const price = ref(props.product.price) copia el número una vez, cuando se ejecuta <script setup>. Después props.product es nuevo, pero price es una caja aparte que nadie cambia.

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 con computed(() => props.product.price * …).

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.

Un computed escribible. computed({ get, set }) da un valor al que también se puede asignar: set convierte el valor nuevo en un cambio del estado real. Se usa poco, por ejemplo para un campo ligado a una parte de un objeto.

computed es perezoso. Si nadie lee total, el cálculo no se ejecuta. Por eso dentro no hay efectos secundarios: no sabes cuándo ni cuántas veces se ejecutará. Para una acción cuando algo cambia existe watch, el módulo siguiente.

La Options API. En el estilo antiguo los valores calculados están en el objeto computed: { total() { … } }. La idea es la misma.

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.

Vue

Test

Ponte a prueba

1. El carrito guarda items. ¿Cuál de estos debería ser un estado aparte?
2. ¿Cuál es la diferencia entre computed y una función llamada en la plantilla?
3. const price = ref(props.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 ·