Как защитно предотвратить мутацию состояния Redux? - PullRequest
0 голосов
/ 10 января 2019

Я пытаюсь кодировать редукторы в моем приложении немного более осторожно, так как моя команда использует существующий код в качестве шаблона для новых изменений. По этой причине я пытаюсь охватить все возможные отклонения от неизменности.

Скажем, у вас есть начальное состояние, которое выглядит следующим образом:

const initialState = {
  section1: { a: 1, b: 2, c: 3 },
  section2: 'Some string',
};

И редуктор, обрабатывающий такое действие:

export default function(state = initialState, action) {
  switch(action.type) {
    case SOME_ACTION:
      return { ...state, section1: action.payload.abc };
  }
  return state;
}

Тогда у вас может быть диспетчер, который делает следующее:

function foo(dispatch) {
  const abc = { a: 0, b: 0, c: 0 };
  dispatch({ type: SOME_ACTION, payload: { abc } });

  // The following changes the state without dispatching a new action
  // and does so by breaking the immutability of the state too.
  abc.c = 5;
}

В этом случае, хотя редуктор следует шаблонам неизменности, создавая поверхностную копию старого состояния и изменяя только измененный бит, диспетчер по-прежнему имеет доступ к action.payload.abc и может его изменять.

Может быть, Redux уже создает глубокую копию всего действия, но не нашел ни одного источника, который бы упоминал об этом. Я хотел бы знать, есть ли подход, чтобы решить эту проблему просто.

Ответы [ 3 ]

0 голосов
/ 10 января 2019

Следует отметить, что в вашем примере мутация не вызовет никаких проблем, если вы просто выполните соответствующее копирование уровня на основе объекта.

Для abc, похожего на { a: 1, b: 2, c: 3 }, вы можете просто сделать поверхностное копирование, в то время как для вложенного объекта { a: { name: 1 } } вам придется его глубоко копировать, но вы все равно можете сделать это явно, без каких-либо библиотек или чего-либо еще.

{
  ...state,
  a: {
    ...action.a
  }
}

В качестве альтернативы вы можете предотвратить мутацию с помощью eslint-plugin-immutable , которая вынудит программиста не писать такой код.

Как вы можете видеть в описании плагина ESLint выше для no-mutation правила :

Это правило так же эффективно, как и использование Object.freeze () для предотвращения мутаций в редукторах Redux. Однако это правило не требует затрат времени выполнения. Хорошей альтернативой мутации объектов является использование синтаксиса распространения объектов, входящего в ES2016.


Еще один относительно простой способ (без использования некоторой библиотеки неизменяемости) активно предотвращать мутации - заморозить ваше состояние при каждом обновлении.

export default function(state = initialState, action) {
  switch(action.type) {
    case SOME_ACTION:
      return Object.freeze({
        ...state,
        section1: Object.freeze(action.payload.abc)
      });
  }
  return state;
}

Вот пример:

const object = { a: 1, b: 2, c : 3 };
const immutable = Object.freeze(object);
object.a = 5;
console.log(immutable.a); // 1

Object.freeze - это мелкая операция, поэтому вам придется вручную заморозить оставшуюся часть объекта или использовать библиотеку, например deep-freeze .

Приведенный выше подход возлагает на вас ответственность за защиту мутации.

Недостаток этого подхода состоит в том, что он увеличивает необходимые когнитивные усилия для программиста, делая явную защиту от мутаций и, таким образом, более подвержен ошибкам (особенно в большой кодовой базе).

При глубоком замораживании могут также наблюдаться некоторые проблемы с производительностью, особенно если вы используете библиотеку, созданную для тестирования / обработки различных крайних случаев, которые ваше конкретное приложение, возможно, никогда не представит.


Более масштабируемый подход заключается во внедрении шаблона неизменяемости в логику кода, который естественным образом подталкивает шаблоны кодирования к неизменяемым операциям.

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

import { Map } from 'immutable';

export default function(state = Map(), action) {
  switch(action.type) {
    case SOME_ACTION:
      return state.merge({ section1: action.payload.abc });
      // this creates a new immutable state by adding the abc object into it
      // can also use mergeDeep if abc is nested
  }
  return state;
}

Другой подход заключается в использовании immer, который скрывает неизменность за кулисами и дает изменчивый apis, следуя принципу copy-on-write .

import produce from 'immer'

export default = (state = initialState, action) =>
  produce(state, draft => {
    switch (action.type) {
      case SOME_ACTION:
        draft.section1 = action.section1;
    })
}

Эта библиотека может вам пригодиться, если вы конвертируете существующее приложение, в котором, вероятно, много кода, выполняющего мутацию.

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

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

0 голосов
/ 10 января 2019

Я сопровождающий Redux.

Сам Redux ничего не делает для предотвращения мутаций в вашем состоянии . Частично это связано с тем, что Redux не знает и не заботится о том, каково его состояние. Это может быть одно число, простые JS-объекты и массивы, Карты и Списки Immutable.js или что-то еще.

Сказав это, существует несколько существующих аддонов разработки, которые ловят случайные мутации .

Я специально предложу вам попробовать наш новый redux-starter-kit пакет . Теперь при использовании функции configureStore() он по умолчанию добавляет промежуточное ПО redux-immutable-state-invariant в ваш магазин в режиме разработки, а также проверяет случайное добавление несериализуемых значений. Кроме того, его утилита createReducer() позволяет определять редукторы, которые упрощают логику неизменяемого обновления, «изменяя» состояние, но обновления фактически применяются неизменно.

0 голосов
/ 10 января 2019

Я полагаю, что в вашем редукторе, если вы создадите новый объект из action.payload.abc, вы можете быть уверены, что любое изменение исходного объекта не повлияет на хранилище редуксов.

case SOME_ACTION:
      return { ...state, section1: {...action.payload.abc}};
...