WTF Is Immutability in JavaScript?
You’ve heard it in code reviews: “don’t mutate the state.” You’ve seen it in React docs, Angular best practices, functional programming guides. But what does it actually mean — and why does it matter so much?
Mutation: The Root of the Problem
Mutation means changing something that already exists in memory.
const user = { name: 'Francisco', role: 'dev' }
user.role = 'lead' // mutation
You took the object that existed and changed it in place. Simple? Yes. Dangerous? Absolutely — especially in large codebases.
Why Mutation Causes Bugs
Unexpected Side Effects
function promote(user) {
user.role = 'lead' // mutates the original
return user
}
const dev = { name: 'Francisco', role: 'dev' }
const lead = promote(dev)
console.log(dev.role) // 'lead' — you didn't want this
console.log(lead.role) // 'lead'
You called promote and got back a lead — but you also silently changed dev. Two variables, one broken reality.
Change Detection Breaks in Frameworks
Angular’s OnPush and React’s React.memo compare references. If you mutate an object, the reference stays the same — the framework thinks nothing changed and doesn’t re-render.
// This breaks OnPush / React.memo
const items = []
items.push(newItem) // same reference, mutation detected = never
// This works
const items = [...oldItems, newItem] // new reference, change detected = always
Immutability: Never Change, Always Replace
The immutable approach is simple: instead of modifying something, you create a new thing with the change applied.
const user = { name: 'Francisco', role: 'dev' }
// Immutable update
const promoted = { ...user, role: 'lead' }
console.log(user.role) // 'dev' — original untouched
console.log(promoted.role) // 'lead'
Immutable Array Patterns
| Operation | Mutable ❌ | Immutable ✅ |
|---|---|---|
| Add item | arr.push(x) | [...arr, x] |
| Remove item | arr.splice(i, 1) | arr.filter((_, idx) => idx !== i) |
| Update item | arr[i] = x | arr.map((item, idx) => idx === i ? x : item) |
| Sort | arr.sort() | [...arr].sort() |
| Reverse | arr.reverse() | [...arr].reverse() |
Object.freeze — Enforced Immutability
If you want JavaScript to actually prevent mutation:
const config = Object.freeze({
apiUrl: 'https://api.example.com',
timeout: 5000,
})
config.timeout = 10000 // silently ignored in sloppy mode, throws in strict mode
console.log(config.timeout) // 5000
Note: freeze is shallow. Nested objects are still mutable. Use structuredClone + freeze for deep immutability, or a library like immer.
Immer: Immutability With Mutable Syntax
When your updates are complex, spread notation gets ugly fast. immer lets you write mutation-style code that produces immutable results:
import { produce } from 'immer'
const state = { users: [{ id: 1, name: 'Francisco', active: true }] }
const nextState = produce(state, (draft) => {
draft.users[0].active = false // looks like mutation, isn't
})
console.log(state.users[0].active) // true — original intact
console.log(nextState.users[0].active) // false
immer is used internally by Redux Toolkit. If you’re using NgRx, the same patterns apply.
The Rule
Simple to state, hard to internalize:
Never modify. Always return something new.
It sounds like extra work. It’s actually less work — because you stop debugging why something changed when you didn’t expect it to. Predictable state is worth every extra spread operator.
¿WTF Es La Inmutabilidad en JavaScript?
Lo escuchaste en code reviews: “no mutes el estado.” Lo viste en la documentación de React, en las buenas prácticas de Angular, en guías de programación funcional. Pero ¿qué significa realmente — y por qué importa tanto?
Mutación: La Raíz del Problema
Mutación significa cambiar algo que ya existe en memoria.
const user = { name: 'Francisco', role: 'dev' }
user.role = 'lead' // mutación
Tomaste el objeto que existía y lo cambiaste en el lugar. ¿Simple? Sí. ¿Peligroso? Absolutamente — especialmente en codebases grandes.
Por Qué La Mutación Genera Bugs
Efectos Secundarios Inesperados
function promote(user) {
user.role = 'lead' // muta el original
return user
}
const dev = { name: 'Francisco', role: 'dev' }
const lead = promote(dev)
console.log(dev.role) // 'lead' — no querías esto
console.log(lead.role) // 'lead'
Llamaste a promote y obtuviste un lead — pero también cambiaste silenciosamente dev. Dos variables, una realidad rota.
La Detección de Cambios Se Rompe en Frameworks
El OnPush de Angular y el React.memo de React comparan referencias. Si mutas un objeto, la referencia queda igual — el framework cree que nada cambió y no re-renderiza.
// Esto rompe OnPush / React.memo
const items = []
items.push(newItem) // misma referencia, nunca se detecta el cambio
// Esto funciona
const items = [...oldItems, newItem] // nueva referencia, cambio detectado siempre
Inmutabilidad: Nunca Cambiar, Siempre Reemplazar
El enfoque inmutable es simple: en vez de modificar algo, creas algo nuevo con el cambio aplicado.
const user = { name: 'Francisco', role: 'dev' }
// Actualización inmutable
const promoted = { ...user, role: 'lead' }
console.log(user.role) // 'dev' — original intacto
console.log(promoted.role) // 'lead'
Patrones Inmutables para Arrays
| Operación | Mutable ❌ | Inmutable ✅ |
|---|---|---|
| Agregar elemento | arr.push(x) | [...arr, x] |
| Eliminar elemento | arr.splice(i, 1) | arr.filter((_, idx) => idx !== i) |
| Actualizar elemento | arr[i] = x | arr.map((item, idx) => idx === i ? x : item) |
| Ordenar | arr.sort() | [...arr].sort() |
| Invertir | arr.reverse() | [...arr].reverse() |
Object.freeze — Inmutabilidad Forzada
Si quieres que JavaScript realmente prevenga la mutación:
const config = Object.freeze({
apiUrl: 'https://api.example.com',
timeout: 5000,
})
config.timeout = 10000 // ignorado silenciosamente en modo normal, error en strict mode
console.log(config.timeout) // 5000
Nota: freeze es superficial. Los objetos anidados siguen siendo mutables. Usa structuredClone + freeze para inmutabilidad profunda, o una librería como immer.
Immer: Inmutabilidad con Sintaxis Mutable
Cuando tus actualizaciones son complejas, la notación de spread se vuelve horrible. immer te permite escribir código de estilo mutación que produce resultados inmutables:
import { produce } from 'immer'
const state = { users: [{ id: 1, name: 'Francisco', active: true }] }
const nextState = produce(state, (draft) => {
draft.users[0].active = false // parece mutación, no lo es
})
console.log(state.users[0].active) // true — original intacto
console.log(nextState.users[0].active) // false
immer se usa internamente en Redux Toolkit. Si usas NgRx, los mismos patrones aplican.
La Regla
Simple de enunciar, difícil de internalizar:
Nunca modificar. Siempre devolver algo nuevo.
Suena a más trabajo. En realidad es menos trabajo — porque dejas de debuggear por qué algo cambió cuando no lo esperabas. El estado predecible vale cada spread operator extra.