Actualizar objetos y arrays sin mutación en React | Nicolás Garzón
React usa referencias para detectar cambios y mantener snapshots coherentes. Mutar un objeto existente modifica también el valor visto por renders anteriores.
TypeScript
Copiar user. name = "Laura" ;
setUser ( user) ; TypeScript
Copiar setUser ( ( current) => ( {
... current,
name: "Laura" ,
} ) ) ; Para datos anidados debes copiar cada nivel modificado.
TypeScript
Copiar setUser ( ( current) => ( {
... current,
address: {
... current. address,
city: "Bogotá" ,
} ,
} ) ) ; TypeScript
Copiar setItems ( ( current) => [ ... current, newItem] ) ; TypeScript
Copiar setItems ( ( current) => current. filter ( ( item) => item. id !== id) ) ; TypeScript
Copiar setItems ( ( current) =>
current. map ( ( item) =>
item. id === updated. id ? updated : item,
) ,
) ; Evita push, pop, splice, sort y reverse directamente sobre estado. Puedes copiar antes:
TypeScript
Copiar const sorted = [ ... items] . sort ( compareProducts) ; Para estructuras complejas, Immer puede ofrecer sintaxis de mutación produciendo copias inmutables, pero no reemplaza comprender referencias.
Copiar solo el objeto raíz y mutar un hijo compartido.
Usar el índice para encontrar identidad.
Guardar estructuras profundamente anidadas sin necesidad.
Una actualización de estado debe producir una nueva referencia para cada parte que cambió y conservar las referencias de lo que no cambió.
Un objeto guardado en state no se vuelve especial. Sigue siendo una referencia de JavaScript. React espera que cada render trate el snapshot recibido como inmutable.
Texto
Copiar render A → user referencia 1
actualización → user referencia 2
render B → nuevo snapshotSi mutas la referencia 1, también cambias retroactivamente el valor que otros lugares podrían observar. Además, reenviar la misma referencia no comunica claramente que existe un valor nuevo.
El spread copia únicamente un nivel:
TypeScript
Copiar const nextUser = { ... user } ;
nextUser. address. city = "Bogotá" ; nextUser.address y user.address siguen apuntando al mismo objeto. Debes copiar cada nivel modificado:
TypeScript
Copiar setUser ( ( current) => ( {
... current,
address: {
... current. address,
city: "Bogotá" ,
} ,
} ) ) ; No necesitas copiar ramas que no cambian. Conservar sus referencias permite compartir estructura y facilita comparaciones.
TypeScript
Copiar setItems ( ( current) => [
... current. slice ( 0 , index) ,
newItem,
... current. slice ( index) ,
] ) ; TypeScript
Copiar setTasks ( ( current) =>
current. map ( ( task) =>
task. id === taskId
? { ... task, completed: ! task. completed }
: task,
) ,
) ; sort() muta. Usa una copia o toSorted:
TypeScript
Copiar const sorted = items. toSorted ( ( a, b) => a. name. localeCompare ( b. name) ) ; TypeScript
Copiar const sorted = [ ... items] . sort ( compareItems) ; TypeScript
Copiar const reversed = items. toReversed ( ) ; O copia antes de reverse().
Actualizar una fila no significa recrear todas sus entidades:
TypeScript
Copiar setProducts ( ( current) =>
current. map ( ( product) =>
product. id === updated. id
? { ... product, ... updated }
: product,
) ,
) ; Los productos sin cambios conservan referencia. Esto puede ayudar a componentes memoizados y selectores, pero el objetivo principal es representar correctamente qué cambió.
Una estructura profundamente anidada puede volver costosas y confusas las actualizaciones:
TypeScript
Copiar type State = {
productsById: Record< string , Product> ;
productIds: string [ ] ;
} ; La normalización ayuda cuando:
La misma entidad aparece en varios lugares.
Se actualiza por ID con frecuencia.
Existen relaciones complejas.
No normalices por costumbre una estructura pequeña y local.
Immer permite escribir una receta aparentemente mutable:
TypeScript
Copiar setState (
produce ( ( draft) => {
draft. orders[ orderId] . status = "paid" ;
} ) ,
) ; Internamente genera una siguiente estructura inmutable y conserva ramas no modificadas. Ventajas:
Reduce spreads anidados.
Hace legibles algunas transiciones complejas.
Añade dependencia y abstracción.
Puede ocultar cuánto cambia la estructura.
No corrige un modelo de datos innecesariamente profundo.
Debes distinguir drafts de objetos normales.
Crear y modificar una estructura nueva durante el mismo cálculo es seguro:
TypeScript
Copiar const nextItems: Item[ ] = [ ] ;
for ( const item of items) {
if ( item. active) nextItems. push ( item) ;
} No estás mutando state; estás construyendo un valor nuevo. La regla no es “nunca uses push”, sino “no modifiques snapshots compartidos”.
No copies todo automáticamente antes de llamar una librería. Primero revisa el contrato:
¿La función muta el argumento?
¿Devuelve una estructura nueva?
¿Conserva referencias?
Cuando una API muta, pasa una copia adecuada o aísla la integración.
Crear objetos nuevos es normal en React. La inmutabilidad no significa clonar toda la aplicación en cada cambio; el structural sharing conserva ramas intactas.
Un clon profundo genérico:
TypeScript
Copiar structuredClone ( state) puede ser innecesariamente costoso, perder tipos especiales según el caso y reemplazar referencias que no cambiaron. Actualiza solo el camino necesario.
El setter se ejecuta pero no hay render esperado.
Un dato cambia en dos componentes a la vez.
DevTools muestra que un snapshot anterior también cambió.
memo no se comporta de forma predecible.
Undo/redo conserva valores incorrectos.
push, splice, sort, reverse sobre state.
Asignaciones a propiedades recibidas por props.
Copias superficiales seguidas de mutación anidada.
Librerías que mutan argumentos.
Si actualizar correctamente la estructura requiere múltiples ramas coordinadas, un reducer puede concentrar reglas:
TypeScript
Copiar function cartReducer ( state: CartState, action: CartAction) : CartState {
} El reducer no elimina la necesidad de inmutabilidad; la hace más local y comprobable.
Los snapshots de state deben tratarse como inmutables.
Copia cada nivel que cambió y conserva el resto.
Métodos mutables son válidos sobre estructuras nuevas, no sobre snapshots compartidos.
La normalización e Immer son herramientas, no requisitos.
Actualiza el camino mínimo; evita clones profundos por reflejo.
¿Por qué { ...user } no basta para modificar user.address.city?
¿Es siempre incorrecto utilizar push?
¿Qué ventaja ofrece conservar referencias de elementos no modificados?
¿Cuándo puede ayudar normalizar el estado?
Ver respuestas
Porque address conserva la referencia del objeto anterior.
No; es seguro sobre una colección nueva que todavía no está compartida.
Representa con precisión qué cambió y permite optimizaciones por referencia.
Cuando muchas vistas comparten entidades y se actualizan frecuentemente por ID.
Lifting state y single source of truth explica dónde debe vivir cada dato para evitar copias desincronizadas.