Lección 3 de 8
Inmutabilidad
Trabaja con estructuras de datos inmutables y evita mutaciones
¿Qué es la inmutabilidad?
La inmutabilidad significa que una vez creados los datos, no se pueden cambiar. En lugar de modificar datos existentes, se crean nuevos datos con los cambios deseados. Esto elimina bugs por mutaciones inesperadas y hace el código más predecible.
❌ El problema de las mutaciones
// Mutation causes unexpected bugs
const user = { name: 'Alice', age: 25 };
function celebrateBirthday(person) {
person.age++; // Mutates the original!
return person;
}
celebrateBirthday(user);
console.log(user.age); // 26 - Original was changed!
// Arrays have the same problem
const numbers = [1, 2, 3];
const sorted = numbers.sort(); // Mutates original!
console.log(numbers); // [1, 2, 3] - Actually sorted!
// This leads to bugs that are hard to track
// "Who changed my data?"
Actualizaciones inmutables de objetos
// Use spread operator to create new objects
const user = { name: 'Alice', age: 25, city: 'NYC' };
// ❌ Mutation
user.age = 26;
// ✅ Immutable update
const updatedUser = { ...user, age: 26 };
console.log(user); // { name: 'Alice', age: 25, city: 'NYC' }
console.log(updatedUser); // { name: 'Alice', age: 26, city: 'NYC' }
// Nested object updates
const state = {
user: { name: 'Alice', address: { city: 'NYC', zip: '10001' } },
settings: { theme: 'dark' },
};
// ✅ Immutably update nested property
const newState = {
...state,
user: {
...state.user,
address: {
...state.user.address,
city: 'Boston',
},
},
};
// Original unchanged
console.log(state.user.address.city); // 'NYC'
console.log(newState.user.address.city); // 'Boston'
Actualizaciones inmutables de arrays
const todos = [
{ id: 1, text: 'Learn FP', done: false },
{ id: 2, text: 'Practice', done: false },
];
// ADD - Use spread or concat
const withNew = [...todos, { id: 3, text: 'Master it', done: false }];
const withNewAlt = todos.concat({ id: 3, text: 'Master it', done: false });
// REMOVE - Use filter
const without2 = todos.filter(todo => todo.id !== 2);
// UPDATE - Use map
const toggleDone = todos.map(todo =>
todo.id === 1 ? { ...todo, done: true } : todo
);
// INSERT AT INDEX
const insertAt = (arr, index, item) => [
...arr.slice(0, index),
item,
...arr.slice(index),
];
// REMOVE AT INDEX
const removeAt = (arr, index) => [
...arr.slice(0, index),
...arr.slice(index + 1),
];
// REPLACE AT INDEX
const replaceAt = (arr, index, item) => [
...arr.slice(0, index),
item,
...arr.slice(index + 1),
];
// SORT (without mutation)
const sorted = [...numbers].sort((a, b) => a - b);
// REVERSE (without mutation)
const reversed = [...numbers].reverse();
Métodos mutables vs no mutables
| Mutables ❌ | No mutables ✅ |
|---|---|
push() |
[...arr, item] |
pop() |
arr.slice(0, -1) |
shift() |
arr.slice(1) |
unshift() |
[item, ...arr] |
sort() |
[...arr].sort() o toSorted() |
reverse() |
[...arr].reverse() o toReversed() |
splice() |
toSpliced() o slice + spread |
arr[i] = x |
with(i, x) o map |
Nota: toSorted(), toReversed(), toSpliced() y with()
son métodos nuevos de ES2023 que devuelven nuevos arrays.
Object.freeze y const
// const prevents reassignment, NOT mutation
const arr = [1, 2, 3];
arr = [4, 5, 6]; // ❌ TypeError: Assignment to constant
arr.push(4); // ✅ Works! Array is mutated
const obj = { name: 'Alice' };
obj = { name: 'Bob' }; // ❌ TypeError
obj.name = 'Bob'; // ✅ Works! Object is mutated
// Object.freeze prevents mutations (shallow)
const frozen = Object.freeze({ name: 'Alice', age: 25 });
frozen.age = 26; // Silently fails (or throws in strict mode)
console.log(frozen.age); // 25
// But freeze is shallow!
const deepObj = Object.freeze({
user: { name: 'Alice' },
});
deepObj.user.name = 'Bob'; // ✅ Works! Nested object not frozen
// Deep freeze helper
function deepFreeze(obj) {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object' && obj[key] !== null) {
deepFreeze(obj[key]);
}
});
return Object.freeze(obj);
}
Inmutabilidad en React
// React relies on immutability for change detection
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn FP', done: false },
]);
// ❌ WRONG: Mutating state directly
const toggleWrong = (id) => {
const todo = todos.find(t => t.id === id);
todo.done = !todo.done; // Mutation!
setTodos(todos); // Same reference, React won't re-render
};
// ✅ CORRECT: Immutable update
const toggleCorrect = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
};
// ✅ Add todo
const addTodo = (text) => {
setTodos([...todos, { id: Date.now(), text, done: false }]);
};
// ✅ Remove todo
const removeTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
}
// useReducer with immutable updates
function reducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return { ...state, todos: [...state.todos, action.payload] };
case 'REMOVE_TODO':
return {
...state,
todos: state.todos.filter(t => t.id !== action.payload)
};
default:
return state;
}
}
Immer para una inmutabilidad sencilla
import { produce } from 'immer';
const state = {
user: { name: 'Alice', address: { city: 'NYC' } },
todos: [{ id: 1, text: 'Learn', done: false }],
};
// Write "mutations" that produce immutable updates!
const newState = produce(state, draft => {
draft.user.address.city = 'Boston';
draft.todos.push({ id: 2, text: 'Practice', done: false });
draft.todos[0].done = true;
});
// Original unchanged
console.log(state.user.address.city); // 'NYC'
console.log(newState.user.address.city); // 'Boston'
// React + Immer
import { useImmer } from 'use-immer';
function App() {
const [state, updateState] = useImmer({ count: 0 });
return (
<button onClick={() => updateState(draft => { draft.count++ })}>
{state.count}
</button>
);
}
✅ Buenas prácticas de inmutabilidad
- • Usa siempre el operador spread para actualizar objetos/arrays
- • Prefiere
map,filteryreducesobre métodos mutables - • Usa
toSorted(),toReversed()para sort/reverse sin mutación - • Considera Immer para actualizaciones anidadas complejas
- • Nunca mutar parámetros de funciones
- • Recuerda:
constevita reasignación, no mutación