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

How to defensively prevent mutation of the Redux state?

我正在尝试对我的应用程序中的缩减程序进行更具防御性的编码,因为我的团队使用预先存在的代码作为新更改的模板。出于这个原因,我试图涵盖所有与不变性的潜在偏差。

假设您的初始状态如下所示:

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

在这种情况下,虽然 reducer 通过创建旧状态的浅拷贝并仅更改更改的位来遵循不变性模式,但调度程序仍然可以访问 action.payload.abc 并可以对其进行更改。

也许 redux 已经创建了整个动作的深层副本,但还没有找到任何提到这一点的来源。想知道有没有办法简单的解决这个问题

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

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

应该注意的是,在您的示例中,如果您只是根据对象执行适当的级别复制,则突变不会导致任何问题。

对于 abc 看起来像 { a: 1, b: 2, c: 3 } 你可以做一个浅拷贝而对于一个嵌套对象 { a: { name: 1 } } 你必须深拷贝它但是你仍然可以明确地这样做没有任何图书馆或任何东西。

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

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

正如您在上面对 no-mutation rule:

的 ESLint 插件的描述中看到的

This rule is just as effective as using Object.freeze() to prevent mutations in your Redux reducers. However this rule has no run-time cost. A good alternative to object mutation is to use the object spread syntax coming in 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 is a shallow operation so you'd have to manually freeze the rest of the object or use a library like deep-freeze.

上述方法将保护突变的责任推给了您。

这种方法的一个缺点是它通过使突变保护显式增加程序员的必要认知努力,因此更容易出现错误(尤其是在大型代码库中)。

深度冻结时也可能会有一些性能开销,特别是如果使用针对 test/handle 您的特定应用程序可能从未引入的各种边缘情况的库。


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

一种方法是使用 Immutable 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 which hides immutability behind the scenes and gives you mutable apis by following the 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;
    })
}

如果您正在转换一个现有的应用程序,该应用程序可能有很多执行突变的代码,那么这个库可能很适合您。

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

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

我是 Redux 维护者。

Redux 本身 does nothing to prevent mutations to your state。部分原因是 Redux 不知道或不关心实际状态是什么。它可以是单个数字、纯 JS 对象和数组、Immutable.js 地图和列表,或其他东西。

话说回来,还有several existing development addons that catch accidental mutations

我会特别建议您在使用 configureStore() 函数时默认在开发模式下试用我们新的 redux-starter-kit package. It now adds the redux-immutable-state-invariant 中间件到您的商店,并检查是否意外添加了不可序列化的值以及。此外,它的 createReducer() 实用程序允许您定义缩减器,通过 "mutating" 状态简化不可变的更新逻辑,但更新实际上是不可变的。