如何防御性地防止Redux状态突变?

时间:2019-01-10 18:16:37

标签: javascript redux immutability

由于我的团队使用预先存在的代码作为新更改的模板,因此我试图在我的应用程序中编写一些简化的代码。因此,我试图涵盖所有与不变性有关的潜在偏差。

假设您的初始状态如下:

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

还有一个reducer处理这样的动作:

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 :(得分:3)

应该注意的是,在您的示例中,如果仅基于对象执行适当的级别复制,则突变不会引起任何问题。

对于看起来像abc的{​​{1}},您可以进行浅表复制,而对于嵌套对象{ a: 1, b: 2, c: 3 },则必须进行深复制,但仍然可以显式地进行复制库或其他任何内容。

{ a: { name: 1 } }

或者,您可以使用eslint-plugin-immutable来防止突变,这会迫使程序员不要编写此类代码。

如您在上面的no-mutation rule的ESLint插件的说明中所见:

  

此规则与使用Object.freeze()来防止Redux reducer中的突变一样有效。但是,此规则没有运行时成本。一个很好的替代对象变异的方法是使用ES2016中提供的对象传播语法。


另一种相对简单的方法(不使用某些不变性库)来主动防止突变是在每次更新时冻结状态。

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

这是一个例子:

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

Object.freeze是一个浅层操作,因此您必须手动冻结对象的其余部分或使用deep-freeze之类的库。

上面的方法将保护您的突变归于您的责任。

这种方法的缺点是,通过明确显示突变保护,它增加了程序员的必要认知工作,因此更容易出现错误(尤其是在大型代码库中)。

深度冻结时可能还会有一些性能开销,特别是如果使用的库用于测试/处理您的特定应用程序可能永远不会引入的各种极端情况。


一种更具可扩展性的方法是将不变性模式嵌入到代码逻辑中,这自然会将编码模式推向不变性操作。

一种方法是使用Immutable JS,其中数据结构本身的构建方式是,对它们的操作始终会创建一个新实例,而不会发生变异。

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

另一种方法是使用immer,它将不可变性隐藏在幕后,并遵循copy-on-write原则为您提供可变的api。

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;
}

如果您要转换的现有应用程序可能包含许多执行变异的代码,那么该库可能对您来说效果很好。

不变性库的缺点是,它增加了不熟悉该库的人进入代码库的障碍,因为现在每个人都必须学习它。

也就是说,一致的编码模式通过明确限制代码的构建方式,减少了认知工作量(每个人都使用相同的模式),并减少了代码混乱因素(防止人们始终发明自己的模式)。 。这自然会减少错误并加快开发速度。

答案 1 :(得分:1)

我是Redux的维护者。

Redux本身does nothing to prevent mutations to your state。部分原因是因为Redux不知道或不在乎实际状态。它可以是一个数字,普通的JS对象和数组,Immutable.js映射和列表等。

话虽如此,有several existing development addons that catch accidental mutations

我将特别建议您尝试使用我们的新redux-starter-kit package。现在,当您使用configureStore()函数时,默认情况下它将以开发模式将redux-immutable-state-invariant中间件添加到您的商店中,并且还检查是否意外添加了不可序列化的值。此外,其createReducer()实用程序使您可以定义化简器,以通过“改变”状态来简化不可变的更新逻辑,但实际上更新是不可变的。

答案 2 :(得分:0)

我相信,在您的化简器中,如果您从action.payload.abc创建一个新对象,那么可以确保原始对象的任何更改都不会影响redux存储。

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