Должен ли я использовать ImmutableJS с Redux?

Redux - это библиотека управления состоянием, которую часто используют в паре с React. О чем меньше говорят, так это о том, как ImmutableJS может принести пользу Redux - и даже React. Сначала я остановлюсь на плюсах и минусах использования Immutable, а затем я расскажу, как интегрировать его в проект Redux / React, в другой статье.

Хорошее

  • Избавляется от ненужного копирования данных. Редукторы Vanilla Redux используют копирование для достижения функциональной чистоты, часто в форме чего-то вроде newState = Object.assign({}, oldState, { newThing: true });. Хотя копирование может показаться безобидным, оно может значительно увеличить объем памяти, необходимый для работы приложения. (Примечание: поверхностное копирование с помощью Object.assign () копирует только верхний уровень объекта, поэтому вложенные объекты не копируются. Однако неглубокие копии не изолируют вложенные объекты от случайной мутации.) К счастью для нас, сборка мусора в движке javascript освободит его для нас во время выполнения. Но вы можете видеть, как это может быть проблематично по мере масштабирования приложения. Неизменяемые структуры данных дают те же преимущества глубокого копирования, изменяя только те части структуры, которые изменились.
  • Эффективное и действенное обнаружение изменений. Обычно, если вы проверяете равенство объектов javascript с помощью оператора тройного равенства oldObject === newObject, вы проверяете только то, была ли изменена вся ссылка на объект. Но это не имеет никакого отношения к тому, совпадают ли фактические вложенные значения или нет. И наоборот, ImmutableJS обновляет ссылки на объекты, чтобы отразить изменения, внесенные во вложенные значения. Это означает, что oldObjectImmutable !== newObjectImmutable действительно указывает, что одно или несколько вложенных значений не равны, а oldObjectImmutable === newObjectImmutable означает, что значения равны. Единственная надежная причина для обнаружения глубоких изменений в собственном javascript - использование дорогих методов, таких как использование JSON.stringify () для всех входных параметров и выполнение сравнения строк.
  • Обеспечивает неизменность языка вместо достижения ее по соглашению. Redux требует, чтобы редукторы никогда не изменяли предыдущий объект состояния, но это делается только по соглашению. В языке или библиотеке нет ничего, что могло бы помешать разработчику случайно сделать это. Вы можете реализовать различные процессы и инструменты, которые помогут предотвратить непреднамеренные мутации, такие как модульные тесты, тщательная экспертная оценка и даже статический анализ, но было бы неплохо убедиться, что этого никогда не происходит в самом коде? С ImmutableJS невозможно ошибиться. Он заставляет операцию всегда возвращать новые коллекции (коллекция - это базовая структура, от которой наследуются сопоставления, списки, записи и т. Д.), Которые полностью изолированы от оригинала.
  • Предоставляет удобные способы изменения глубоко вложенных свойств. Обновление дерева состояний на верхнем уровне довольно просто с помощью Object.assign (), но вложенные обновления более сложны и подвержены ошибкам. Обычно проще создать потенциально дорогостоящую глубокую копию состояния, изменить копию и вернуть ее в конце функции редуктора. Меня это не устраивает. Immutable предоставляет методы .setIn () и getIn (), которые предоставляют простые способы перехода по дереву к точному узлу, который вы пытаетесь получить или обновить. Что замечательно, ImmutableJS всегда возвращает относительно дешевую копию всей коллекции, поэтому вы можете связать обновления довольно чистым способом - как только вы перестанете использовать строку, что я представляю как недостаток ниже.
[SOME_ACTION]: (state, action) => 
  state
    .set('loading', false)
    .setIn(['down', 'we', 'go'], action.payload.id)
    .setIn(['this', 'is', 'easy'], action.payload.response),
  • Может быть постепенно введен в существующую кодовую базу *. Я ставлю звездочку рядом с этим, потому что с Redux это тривиально, но с React не обязательно - по причинам, о которых я расскажу ниже. С Redux все, что вам нужно сделать, это инициализировать состояние неизменяемой записью или картой, предпочтительно записью.
import { handleActions } from 'redux-actions';
import Immutable from 'immutable';

import {
  SHOW_MODAL_DIALOG,
} from './modalDialogActions.js';

const ModalDialogRecord = new Immutable.Record({
  show: false,
  titleText: '',
  bodyComponent: undefined,
  busy: false,
});

const initialState = new ModalDialogRecord();

const actions = {
  [SHOW_MODAL_DIALOG]: (state, action) =>
    state
      .set('show', true)
      .merge(Immutable.Map(action.payload)),
};

export default handleActions(actions, initialState);

Плохо

  • Увеличивает интеллектуальную нагрузку, необходимую разработчикам для реализации функции. Это реально. Как фронтенд-разработчики, нам уже приходится манипулировать десятками (сотнями ?!) библиотек и инструментов. Нужно ли нам добавить еще один, чтобы вызвать полный паралич анализа? Раньше я цеплялся за самую последнюю и лучшую ‹вставку новой библиотеки› .js, но через некоторое время ее стало невозможно поддерживать. Вот почему стоимость новой блестящей вещи должна перевешивать затраты на управление множеством блестящих вещей.
  • Уродливый синтаксис для получения и настройки. Чтобы получить доступ к различным свойствам на неизменяемой карте, вам нужно использовать строки для всех свойств. Если один из них введен с ошибкой, возвращается undefined. Также нецелесообразно создавать строковые константы для всех ключей. Если вы соглашаетесь на использование неизменяемых записей, это смягчается, поскольку записи поддерживают точечную нотацию для доступа к свойствам и наследуют все методы и свойства карт. Но допустим, вам нужно получить глубоко вложенное значение в списке записей. Вы, вероятно, все равно будете использовать строки state.getIn(['aListOfRecords', 0, 'someProp'], 7). Итак, чтобы использовать Immutable, вам нужно принять строковый синтаксис как реальность. (Примечание: с помощью инструмента статической типизации, такого как Flow или Typescript, ключи, на которые ссылается строковый синтаксис, проверяются.)
  • Немного сложнее отлаживать код. Основной вклад здесь - неизменяемые структуры, обернутые не только в то, что они хранят. Поэтому, когда вы отлаживаете и попадаете в точку останова, значения не сразу читаются. Вы должны вызвать .toJS () в консоли, чтобы увидеть их. Теперь есть плагины и инструменты для решения этой проблемы, но они добавляют больше интеллектуальной нагрузки, и у вас не всегда есть к ним доступ.
  • Часто несовместим с существующими компонентами React и без некоторого рефакторинга. Любой компонент, который имел дело с собственными структурами javascript, вероятно, потребует некоторой степени рефакторинга. Использование неизменяемых записей значительно упрощает рефакторинг, но вы не можете обойтись без разных неизменяемых списков. К счастью, Immutable поддерживает соглашения ES6, поэтому, если вы уже им следуете, это неплохо. Если вы используете такие библиотеки, как Lodash, вам потребуется больше усилий по рефакторингу, но все же выполнимо. Обычно это бывает примерно так:
// Lodash
const foundSomething = _.find(stuffInArray, o => o.id === someId);
// versus
// ImmutableJS/ES6
const foundSomething = stuffInList.find(o => o.id === someId);
  • Может не привести к значительному повышению производительности и в некоторых случаях действительно может замедлить работу приложения. Помните, я говорил, что копирование - это плохо? Что ж, на самом деле это не оказывает заметного влияния, если размер структуры данных небольшой. Это может быть большинство случаев, с которыми вы работаете. Так стоят ли накладные расходы на Immutable? Возможно нет. Кроме того, будут случаи, когда вам придется использовать .toJS () - однако это не должно быть нормой - чтобы сделать его совместимым с другой библиотекой или чем-то еще. Использование .toJS () довольно дорого и будет намного медленнее, чем копирование.

Честно говоря, большинство недостатков ImmutableJS можно преодолеть путем создания неизменяемых структур данных на языке javascript. Если бы существовали простые способы регистрации значений и если бы большинство библиотек обрабатывали неизменяемые данные, не было бы никакого мысленного переключения контекста. (Бывают моменты, когда чисто функциональные языки transpile-to-js выглядят заманчиво, но все еще остаются проблемы с совместимостью и поддержкой.) Инженеры внешнего интерфейса обречены жить в постоянном контролируемом хаосе.

На работе у нас есть большое корпоративное веб-приложение, которое управляет большим количеством состояний, и масштабируемость была для нас большой проблемой. Мы также хотели иметь возможность оптимизировать производительность рендеринга компонентов с помощью дешевого обнаружения изменений. Вот почему мы выбрали ImmutableJS, но он может не подходить вам и вашей команде.

Изменить: добавлены дополнительные разъяснения по копированию данных.

Изменить: Обеспечено исправление и разъяснение эффектов неглубокого копирования через Object.assign ()