En React actualizas un array o un objeto del estado creando uno nuevo y pasándolo al setter. Nunca cambies el valor existente en su lugar: usa spread (...), map, filter o slice para construir una copia con tu cambio, y luego llama a setItems(copy).
[...items, { ... }] es un array nuevo con un elemento más, y filter devuelve un array nuevo sin el elemento eliminado. Ambos dejan el array viejo intacto.
Por qué mutar no renderiza
Cuando llamas a un setter, React compara el valor nuevo con el viejo usando Object.is. Para arrays y objetos eso compara referencias, no contenidos. Si cambias un array con push y devuelves el mismo array, React ve la misma referencia, decide que nada cambió y omite el renderizado.
Haz clic en "push" varias veces. La consola muestra el array creciendo, pero no aparece ninguna línea de renderizado nueva y el texto se queda en a. Luego haz clic en "spread": ocurre un renderizado y todos los elementos agregados con push aparecen a la vez, porque estuvieron en el array todo el tiempo. Mutar no lanza un error, simplemente deja la pantalla desactualizada hasta que otra cosa provoca un renderizado. Haz clic en "push" una vez más después de eso y esta vez puede que sí renderice: React no siempre puede omitir el trabajo de antemano, así que una mutación aparece unas veces y otras no. Esa imprevisibilidad es la razón para no mutar el estado.
Arrays: métodos que usar y que evitar
La regla es simple: usa métodos que devuelven un array nuevo y evita los métodos que cambian el array sobre el que se llaman.
| Objetivo | Evitar (muta) | Usar (devuelve un array nuevo) |
|---|---|---|
| Agregar | push, unshift | [...arr, item], [item, ...arr] |
| Quitar | pop, shift, splice | filter, slice |
| Reemplazar | arr[i] = x, splice | map, with(i, x) |
| Insertar | splice | [...arr.slice(0, i), x, ...arr.slice(i)], toSpliced |
| Ordenar, invertir | sort, reverse | toSorted, toReversed, o copiar primero |
toSorted, toReversed, toSpliced y with son métodos de array más nuevos (ES2023), compatibles con todos los navegadores principales actuales desde 2023. Hacen el mismo trabajo que sus primos mutables, pero sobre una copia.
Actualizar un elemento con map
Para cambiar un elemento, recorre el array con map y devuelve un objeto nuevo para el elemento que quieres, y el objeto original para todos los demás.
La parte { ...todo, done: !todo.done } importa. Un error común es copiar el array pero cambiar el objeto que contiene:
const next = [...todos];
next[0].done = true; // next is new, but next[0] is the same object as todos[0]
setTodos(next);
Esto renderiza, así que parece correcto, pero el array viejo y el nuevo ahora comparten un objeto modificado. Cualquier cosa que compare el elemento viejo con el nuevo (un hijo con React.memo, un efecto que depende del elemento) no ve ninguna diferencia. Copia el elemento que cambias.
Insertar en un índice
No existe un insert que no mute, así que corta el array en dos y pon el nuevo elemento entre las mitades. toSpliced(index, 0, item) hace lo mismo en una sola llamada.
Pon el índice en 0 e inserta otra vez: la nueva C va al principio. Reemplaza la expresión next por letters.toSpliced(index, 0, 'C') y el resultado es el mismo.
Ordenar e invertir
sort y reverse cambian el array sobre el que se llaman, aunque también lo devuelven. Eso convierte setItems(items.sort()) en una mutación seguida de un set con la misma referencia, que es el error del principio de esta página. Ordena una copia.
[...scores].reverse() funciona porque la propagación crea primero la copia y luego reverse cambia solo esa copia.
Actualizar objetos y objetos anidados
Los objetos siguen la misma regla. Propaga el objeto viejo en uno nuevo y sobrescribe los campos que cambian. Para un objeto anidado, propaga en cada nivel del camino hasta el campo: una sola propagación copia solo el nivel superior, así que el objeto interior seguiría siendo el viejo.
Cada nivel que copias es un objeto nuevo; cada nivel que no tocas sigue siendo el mismo objeto. Eso es lo que hace barata la propagación: si el estado también tuviera una larga lista de pedidos, { ...user } copiaría la referencia a la lista, no la lista en sí.
Si un objeto en el estado está anidado tres o cuatro niveles y sigues escribiendo largas cadenas de propagaciones, suele ser señal de que el estado podría ser más plano. Guardar los elementos en un diccionario por id, o dividir un objeto grande en varias llamadas a useState, mantiene cada actualización corta. Cuando muchos eventos cambian una misma estructura, un reducer también ayuda (consulta useReducer).
structuredClone vs spread
structuredClone(user) hace una copia profunda en una sola llamada, así que luego puedes cambiar la copia libremente:
const next = structuredClone(user);
next.address.city = 'Oslo';
setUser(next);
Es correcto, pero copia el objeto entero cada vez, incluidas las partes que no cambiaste. Eso cuesta tiempo con estados grandes, y significa que cada objeto anidado recibe una referencia nueva, así que un hijo memoizado que recibe user.orders se vuelve a renderizar aunque los pedidos no hayan cambiado. structuredClone además lanza un error con funciones, y las instancias de clases vuelven como objetos simples. Spread copia solo el camino que cambias, así que es la opción habitual; deja structuredClone para la rara actualización que de verdad reescribe la mayor parte del objeto.
Escribir mutaciones con Immer
Immer es una pequeña librería que te permite escribir código que parece una mutación. Te entrega un borrador, registra lo que cambias y produce un valor inmutable nuevo copiando solo las partes que cambiaron. El paquete use-immer lo envuelve en un hook:
import { useImmer } from 'use-immer';
function Profile() {
const [user, updateUser] = useImmer({
name: 'Ada',
address: { city: 'London' },
});
function moveTo(city) {
updateUser((draft) => {
draft.address.city = city;
});
}
// ...
}
Immer no está disponible en los editores de esta página, que solo cargan React. Vale la pena agregarlo cuando tu estado está muy anidado y las propagaciones se vuelven difíciles de leer. Para un estado plano, spread y map bastan, y son lo que verás en la mayor parte del código de React.
Preguntas frecuentes
¿Cómo agrego un elemento a un array en el estado de React?
Crea un array nuevo que contenga los elementos viejos más el nuevo y pásalo al setter: setItems([...items, newItem]). No llames a items.push(newItem), porque eso cambia el array existente y React ve la misma referencia.
¿Por qué mi componente no se vuelve a renderizar cuando actualizo un array?
Lo más probable es que hayas cambiado el array en su lugar (push, splice, sort o arr[i] = x) y devuelto el mismo array. React compara el valor viejo y el nuevo con Object.is, encuentra el mismo objeto y omite el renderizado. Pasa un array nuevo.
¿Cómo actualizo un objeto dentro de un array en React?
Usa map y devuelve un objeto nuevo para el elemento que cambia, y el elemento original para todos los demás: setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t)).
¿Cómo actualizo un objeto anidado en el estado de React?
Copia cada nivel del camino hasta el campo que cambias: setUser({ ...user, address: { ...user.address, city: 'Oslo' } }). Una sola propagación copia solo el nivel superior, así que el objeto interior seguiría compartido.
¿Debo usar structuredClone o el operador spread para el estado de React?
Prefiere spread. Copia solo lo que cambia y deja las partes sin tocar como los mismos objetos, lo que ayuda a React.memo y a las comparaciones. structuredClone copia todo cada vez, lo que es más sencillo de escribir pero más lento con estados grandes y rompe la igualdad de referencias en las partes que no cambiaron.