Learning Book

Иммутабельность

Зачем иммутабельность

Мутация объектов создаёт неочевидные связи в коде — изменение в одном месте неожиданно влияет на другое.

// Проблема мутации
const user = { name: 'Алиса', age: 25 }
const admins = [user]

function birthday(user) {
  user.age++  // мутируем! Изменяется и в admins!
  return user
}

birthday(user)
console.log(admins[0].age) // 26 — неожиданно!

Создание нового объекта вместо мутации

// Иммутабельный подход: создаём новый объект
function birthday(user) {
  return { ...user, age: user.age + 1 }  // новый объект
}

const user = { name: 'Алиса', age: 25 }
const admins = [user]

const olderUser = birthday(user)
console.log(user.age)       // 25 — не изменился
console.log(olderUser.age)  // 26 — новый объект
console.log(admins[0].age)  // 25 — тоже не изменился

Иммутабельные паттерны для объектов

const state = { user: { name: 'Алиса', settings: { theme: 'dark' } } }

// Обновление вложенного поля (иммутабельно)
const newState = {
  ...state,
  user: {
    ...state.user,
    settings: {
      ...state.user.settings,
      theme: 'light'  // только это изменилось
    }
  }
}

// Удаление поля
const { theme, ...withoutTheme } = state.user.settings
// withoutTheme не содержит theme

Иммутабельные паттерны для массивов

const items = [1, 2, 3, 4, 5]

// Добавить элемент
const withNew = [...items, 6]           // в конец
const withFirst = [0, ...items]         // в начало

// Удалить элемент (по индексу)
const withoutSecond = items.filter((_, i) => i !== 1)  // [1, 3, 4, 5]

// Обновить элемент
const updated = items.map((item, i) => i === 2 ? 99 : item) // [1, 2, 99, 4, 5]

// Вставить в середину
const inserted = [
  ...items.slice(0, 2),
  99,
  ...items.slice(2)
]  // [1, 2, 99, 3, 4, 5]

Object.freeze

Object.freeze делает объект «замороженным» — свойства нельзя изменить:

const config = Object.freeze({
  apiUrl: 'https://api.example.com',
  timeout: 5000
})

config.apiUrl = 'другой url' // тихо игнорируется (в strict mode — TypeError)
console.log(config.apiUrl) // 'https://api.example.com'

// Внимание: freeze поверхностный!
const nested = Object.freeze({ inner: { value: 1 } })
nested.inner.value = 99  // работает! inner не заморожен
Object.freeze — поверхностная заморозка. Для глубокой нужна рекурсия или библиотека (immer, immutable-js). В React-экосистеме обычно хватает поверхностной иммутабельности (новые ссылки для изменённых объектов).

Immer — иммутабельность через мутации

Библиотека immer позволяет писать «мутирующий» код, но под капотом создаёт иммутабельные обновления:

import produce from 'immer'

const state = { users: [{ id: 1, name: 'Алиса', active: true }] }

// Пишем как будто мутируем, получаем новый объект
const newState = produce(state, draft => {
  draft.users[0].active = false  // выглядит как мутация
  draft.users.push({ id: 2, name: 'Боб', active: true })
})

// state не изменился
console.log(state.users[0].active)    // true
console.log(newState.users[0].active) // false