Back

Неизменяемое состояние простым способом: разбираемся с Immer

Неизменяемое состояние простым способом: разбираемся с Immer

Обновление вложенного состояния в JavaScript без мутаций — утомительная задача. Вы распространяете объекты на каждом уровне, отслеживаете, какие ссылки изменились, и надеетесь, что случайно ничего не мутировали по пути. Для простого вложенного обновления вам может потребоваться написать пять строк аккуратной логики копирования.

Immer решает эту проблему другим подходом: пишите код, который выглядит как мутация, но автоматически создаёт неизменяемое состояние. В этой статье объясняется, как работает неизменяемость на основе прокси в Immer, почему Redux Toolkit использует его внутри, и какие практические нюансы следует знать перед его внедрением.

Ключевые выводы

  • Immer позволяет писать код в стиле мутаций, который создаёт неизменяемое состояние через функцию produce и черновики на основе прокси
  • Структурное разделение гарантирует, что только изменённые ветви получают новые ссылки, сохраняя производительность для проверок повторного рендеринга в React и Redux
  • Redux Toolkit использует Immer внутри, поэтому редьюсеры createSlice автоматически обрабатывают неизменяемость без дополнительных импортов
  • Остерегайтесь распространённых ошибок: не переназначайте сам черновик, избегайте смешивания мутаций черновика с возвращаемыми значениями и будьте осторожны с экземплярами классов

Как Immer создаёт неизменяемое состояние

Основной API Immer — это функция produce. Вы передаёте ей текущее состояние и функцию-«рецепт». Внутри этого рецепта вы получаете draft — прокси, оборачивающий ваше исходное состояние. Вы изменяете черновик, используя обычные JavaScript-мутации. Когда рецепт завершается, Immer генерирует новое неизменяемое состояние, отражающее ваши изменения.

import { produce } from "immer"

const baseState = {
  user: { name: "Alice", settings: { theme: "dark" } }
}

const nextState = produce(baseState, draft => {
  draft.user.settings.theme = "light"
})

// baseState.user.settings.theme по-прежнему "dark"
// nextState.user.settings.theme теперь "light"

Ментальная модель проста: представьте, что вы мутируете, но Immer обрабатывает неизменяемые обновления за кулисами.

Неизменяемость на основе прокси и структурное разделение

Immer использует JavaScript-объекты Proxy для перехвата ваших операций чтения и записи в черновике. Когда вы обращаетесь к вложенному свойству, Immer лениво создаёт прокси для этого пути. Когда вы записываете в свойство, Immer помечает этот узел (и его предков) как изменённый и создаёт поверхностные копии только там, где это необходимо.

Этот подход копирования при записи обеспечивает структурное разделение. Неизменённые части вашего дерева состояния остаются теми же ссылками на объекты. Только изменённые ветви получают новые ссылки. Это важно для React и Redux, потому что проверки равенства ссылок определяют, будут ли компоненты перерисованы.

Современный Immer (v10+) требует нативной поддержки Proxy — нет резервного варианта для ES5. Это нормально для современных браузеров и Node.js, но стоит учитывать, если вы работаете с необычными окружениями.

Интеграция Immer в Redux Toolkit

Если вы используете Redux Toolkit, вы уже используете Immer. createSlice в RTK автоматически оборачивает вашу логику редьюсера функцией produce. Вы пишете «мутирующий» код в case-редьюсерах, а RTK обрабатывает неизменяемость:

import { createSlice } from "@reduxjs/toolkit"

const todosSlice = createSlice({
  name: "todos",
  initialState: [],
  reducers: {
    addTodo: (state, action) => {
      state.push(action.payload) // Выглядит как мутация, но это безопасно
    },
    toggleTodo: (state, action) => {
      const todo = state.find(t => t.id === action.payload)
      if (todo) todo.completed = !todo.completed
    }
  }
})

Эта интеграция Immer в Redux Toolkit устраняет шаблонный код с оператором spread, который делал редьюсеры в vanilla Redux многословными. Вам не нужно импортировать produce отдельно — RTK настраивает его за вас.

Автоматическая заморозка и поведение итерации

Immer по умолчанию автоматически замораживает созданное состояние (используя Object.freeze). Это помогает отловить случайные мутации во время разработки, но добавляет накладные расходы. Вы можете отключить это в продакшене через конфигурацию, если профилирование покажет, что это имеет значение:

import { setAutoFreeze } from "immer"

setAutoFreeze(false) // Отключить в продакшене для производительности

Современный Immer по умолчанию использует нестрогую итерацию для производительности: в черновиках перебираются только перечисляемые строковые ключи. Обычно это то, что вам нужно для состояния приложения, но это означает, что символьные ключи и неперечисляемые свойства пропускаются, если вы явно не включите строгую итерацию. В основном это важно в крайних случаях или при низкоуровневой манипуляции данными.

Практические нюансы для неизменяемых обновлений в JavaScript

Несколько подводных камней сбивают с толку разработчиков, новых в Immer:

Не переназначайте сам черновик. Изменение draft.property работает. Переназначение draft = newValue не даёт полезного эффекта, потому что вы только изменяете локальную привязку параметра.

Возвращаемые значения имеют значение. Если ваш рецепт возвращает значение, Immer использует его как новое состояние вместо изменённого черновика. Возврат undefined обрабатывается так же, как отсутствие возврата, но смешивание мутаций черновика и явных возвратов — распространённый источник ошибок. Используйте один подход или другой.

Классы и экзотические объекты требуют осторожности. Immer лучше всего работает с простыми объектами, массивами, Maps и Sets. Экземпляры классов не проксируются автоматически корректно. Вам может потребоваться пометить их как неизменяемые или обрабатывать отдельно (как описано в официальной документации о подводных камнях).

Производительность не бесплатна. Immer подходит для типичного состояния UI — форм, списков, пользовательских настроек. Для горячих циклов, обрабатывающих тысячи элементов за кадр, измеряйте производительность, прежде чем предполагать, что Immer не добавляет накладных расходов. Механизм прокси имеет свою цену.

Лучше всего работает древовидное состояние. Immer предполагает, что ваше состояние — это дерево. Циклические ссылки или общие ссылки на объекты между ветвями могут привести к неожиданным результатам.

Заключение

Immer отлично подходит для умеренно сложных, вложенных обновлений состояния — именно то, с чем вы сталкиваетесь в React-компонентах и Redux-редьюсерах. Он устраняет шаблонный код, отлавливает случайные мутации и бесшовно интегрируется с Redux Toolkit.

Для простого плоского состояния нативные операторы spread работают нормально. Для критичной к производительности обработки данных сначала проведите бенчмарки. Но для типичного управления состоянием во фронтенде подход Immer на основе прокси предлагает лучший баланс между опытом разработчика и корректностью.

Часто задаваемые вопросы

Да. Вы можете обернуть свои обновления состояния напрямую с помощью produce или использовать хук useImmer из пакета use-immer. Это даёт вам тот же синтаксис в стиле мутаций для локального состояния компонента, который Redux Toolkit предоставляет для глобального состояния.

Immer имеет отличную поддержку TypeScript. Функция produce автоматически выводит типы из вашего базового состояния, а объекты черновиков сохраняют правильную типизацию. Вы получаете полное автодополнение и проверку типов при написании кода в стиле мутаций.

Immer нативно поддерживает Maps и Sets. Вы можете использовать стандартные методы Map и Set, такие как set, delete и add, непосредственно на черновиках в современных версиях без какой-либо дополнительной конфигурации.

Только если профилирование показывает, что это вызывает проблемы с производительностью. Автоматическая заморозка помогает отлавливать ошибки, выбрасывая исключения при случайных мутациях. Для большинства приложений накладные расходы незначительны, но высокочастотные обновления могут выиграть от отключения через setAutoFreeze false.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay