Ir al contenido
wannacode

Actualizar objetos y arrays

En resumen: Un objeto o un array en el estado de React se actualiza con una copia nueva (spread, map, filter), porque React compara referencias y no ve un cambio hecho en el viejo.

Problema

«Quitar» no quita nada

El carrito tiene tres líneas, cada una con un botón «Quitar». Pulsa «Quitar» en el pan.

React

La consola dice líneas: 2. La pantalla muestra las tres líneas y el total viejo. Los datos cambiaron, la pantalla no los sigue, y no hay ningún error.

remove saca la línea con splice y llama a setCart(cart). Se pasó el valor nuevo. ¿Por qué React no dibuja?

La idea

Cómo ve la biblioteca un cambio

Con un número está claro: 1 y 2 son distintos. Un array y un objeto, en cambio, son referencias: la variable apunta a un objeto en la memoria. Hay dos preguntas. ¿La variable apunta a otro objeto? ¿Y cambió el contenido del mismo objeto? Cada biblioteca pregunta a su manera, y aquí la diferencia se nota en el código.

React hace solo la primera pregunta. Con setCart(next) compara el estado viejo y el nuevo con Object.is. Para un array eso significa «¿es el mismo objeto?», no «¿tiene el mismo contenido?».

splice cambia el contenido del mismo array. setCart(cart) pasa la misma referencia, Object.is dice «el mismo», y React se salta el render. Por eso en React el array y los objetos del estado no se cambian en el sitio. Haces un array nuevo, y React ve una referencia nueva. Esto se llama una actualización inmutable (immutable update).

Míralo

Mira: el mismo array o uno nuevo

const [cart, setCart] = useState([bread]);
function addMilk() {
cart.push(milk);
setCart(cart);
}
function addCheese() {
setCart([...cart, cheese]);
}
Paso a paso, en palabras
  1. cart apunta a un array con una línea. React guarda esta referencia como estado del componente, y la pantalla muestra el pan.
  2. cart.push(milk) añade una línea al mismo array. El contenido cambió, pero es el mismo objeto.
  3. setCart(cart) pasa la misma referencia. React compara la vieja y la nueva con Object.is: el mismo objeto.
  4. El mismo significa «nada nuevo»: React se salta el render. La leche está en el array, pero no en la pantalla.
  5. [...cart, cheese] hace un array nuevo: una copia de las líneas más el queso.
  6. setCart con el array nuevo: Object.is dice «distintos», y React vuelve a dibujar.
  7. Ahora la pantalla muestra las tres líneas. También la leche, que hasta ahora estaba escondida en el array viejo.
  8. La regla en React: no cambies el array del estado, dale uno nuevo.
La leche está en el array desde el push, pero aparece en la pantalla solo junto con el queso. El código con una mutación no falla sin más. Esconde un cambio que sale después, en una acción totalmente distinta.

La idea

Un array nuevo, un objeto nuevo

El carrito tiene tres acciones: añadir una línea, cambiar la cantidad de una línea, quitar una línea.

// añadir: un array nuevo con las líneas viejas y la nueva
setCart([...cart, { ...product, qty: 1 }]);
// cambiar una línea: un array nuevo, la línea cambiada es un objeto nuevo
setCart(cart.map((line) => (line.id === id ? { ...line, qty: line.qty + 1 } : line)));
// quitar: un array nuevo sin la línea
setCart(cart.filter((line) => line.id !== id));
  • [...cart, x] es un array nuevo: los elementos viejos y x al final.
  • { ...line, qty: 5 } es un objeto nuevo: los campos de line, con qty cambiado.
  • map y filter siempre devuelven un array nuevo. Por eso son las herramientas principales aquí.

Pruébalo

Arregla «Quitar»

Este es el carrito del principio. Arregla remove para que la línea desaparezca y el total se calcule de nuevo. La comprobación quita el pan y después el queso.

React

Pruébalo

Encuentra el error

«Quitar» no quita. ¿Qué línea tiene la culpa?

Elige la línea con el error.

La idea

Una línea del array

La clienta pulsa «+» en el pan. Tiene que cambiar el qty de un objeto, y el objeto está en un array.

cart.find(...).qty++ cambia el objeto en el sitio: es el mismo objeto que React guarda como estado viejo. El array es el mismo, así que React no vuelve a dibujar. El número no desaparece: llega a la pantalla en el siguiente render por otro motivo, y la cantidad «salta».

Cuidado también con las copias. [...cart] es un array nuevo, pero las líneas que contiene son los mismos objetos. Esto es una copia superficial (shallow copy): solo se copia el primer nivel. copy[0].qty++ cambia la línea también en el estado viejo. La línea cambiada tiene que ser un objeto nuevo: { ...line, qty: line.qty + 1 }. Las líneas que no cambian siguen siendo los mismos objetos, y está bien.

Más precisamente, para curiosos

Object.is. Como ===, con dos diferencias: Object.is(NaN, NaN) es true, y Object.is(0, -0) es false. Con arrays y objetos, los dos comparan referencias.

Objetos anidados. Cada nivel del camino hasta el cambio es nuevo: { ...order, address: { ...order.address, city: 'Varna' } }. Los niveles de al lado siguen siendo los mismos.

structuredClone(cart) hace una copia profunda: el array y cada objeto que contiene. Funciona, pero copia todo en cada cambio y no puede copiar funciones. Para una línea, map y ... salen más baratos.

Immer. La biblioteca Immer te deja escribir como si mutaras (draft[0].qty++) y hace los objetos nuevos por ti. No está en el sandbox.

En Vue también funciona. El array nuevo es correcto también en Vue: cart.value = cart.value.filter(...). Al revés no vale: la mutación, normal en Vue, no se ve en React.

Pruébalo

«+» y «−»

Escribe more y less. «+» sube la cantidad de la línea en 1, y «−» la baja en 1. Cuando la cantidad es 1, «−» quita la línea. El total sigue a la cantidad.

React

Ejercicio

Catálogo y carrito

El catálogo tiene cuatro productos con un botón «Añadir» (el nombre para el lector de pantalla: «Añadir Pan blanco»). Escribe add y remove. El primer clic en «Añadir» hace una línea con cantidad 1, y los siguientes suben la cantidad de la misma línea en lugar de hacer una nueva. «Quitar» quita la línea. Cuando el carrito está vacío, dice «El carrito está vacío».

React

Test

Ponte a prueba

1. cart.push(line); setCart(cart); ¿Por qué no cambia la pantalla?
2. ¿Cuál sube bien la cantidad de una línea?
3. const next = [...cart]; next[0].qty++; setCart(next); ¿Qué está mal?
4. ¿Cómo quitas una línea sin cambiar el array del estado?

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