Skip to main content

Redux 风格指南

介绍​

这是官方的 Redux 代码编写风格指南。它列出了我们推荐的模式、最佳实践以及编写 Redux 应用程序的建议方法。

Redux 核心库和大部分 Redux 文档都是非强制性的。Redux 有许多使用方式,很多情况下并不存在单一的“正确”的做法。

然而,时间和经验表明,对于某些话题,某些方法比其他方法更有效。另外,许多开发者也希望我们提供官方指导以减少决策疲劳。

基于此,我们整理了这份建议清单,帮助你避免错误、纠结和反面模式。我们也理解不同团队有不同的偏好,不同项目有不同的需求,所以没有哪份风格指南能适用于所有情况。我们鼓励你遵循这些建议,但请花时间评估自身情况,决定是否适合你的需求。

最后,感谢 Vue 文档作者编写了 Vue 风格指南页面,本页的写作受到了它的启发。

规则分类​

我们将这些规则划分为三个类别:

优先级 A:必需​

这些规则有助于防止错误,因此务必学习并遵守。 例外情况可能存在,但应极为罕见,并且只能由对 JavaScript 和 Redux 都有专家级知识的人决定。

优先级 B:强烈推荐​

这些规则被发现能提高大多数项目的可读性和/或开发者体验。如果违反规则,代码依然能运行,但应尽量避免且有充分理由。只要合理可能,务必遵守这些规则。

优先级 C:推荐​

当存在多个同等优秀的选项时,可任选其一以保证一致性。此类别中,我们描述每个可接受的选项并建议一个默认选择。这意味着你可以在自己的代码库中作出不同选择,只要保持一致并有合理理由。当然,请确保你有合理理由!

优先级 A 规则:必需​

不要直接修改 State​

修改 state 是 Redux 应用中最常见的 bug 源,包括组件无法正确重新渲染,也会破坏 Redux DevTools 中的时间旅行调试。应始终避免实际修改 state 值,无论是在 reducers 内部还是在其他所有应用代码中。

Redux Toolkit 的 configureStore 内置了不可变性检查中间件,可在开发时捕获修改操作;createSlice 使用 Immer,确保状态更新不会意外修改真实数据。

注意:修改_复制_的现有值是允许的——这是编写不可变更新逻辑的正常部分。如果你使用 Immer 进行不可变更新,编写类似“修改”的逻辑是可以接受的,因为实际数据本身未被修改——Immer 会安全追踪更改,并在内部生成不可变更新的值。

Reducers 不能有副作用​

Reducer 函数应_仅_依赖其 state 与 action 参数,并仅基于这两个参数计算并返回新的 state 值。不得执行任何异步逻辑(AJAX 调用、定时器、Promise)、生成随机值(Date.now()、Math.random())、修改 reducer 外部变量,或执行其他影响 reducer 作用域外部事物的代码。

注意:Reducer 调用定义在外部的函数(例如库导入或工具函数)是被允许的,前提是它们遵守相同规则。

Detailed Explanation

此规则的目的是保证调用 reducer 时其行为可预测。例如,时间旅行调试可能会多次调用 reducer 处理早期动作以生成“当前”状态。如果 reducer 有副作用,这些副作用将在调试过程中执行,导致应用表现异常。

此规则存在一些灰色地带。严格来说,类似 console.log(state) 的代码是副作用,但实际上对应用行为无影响。

不要在 State 或 Actions 中放置不可序列化的值​

避免将不可序列化的值(如 Promise、Symbol、Map/Set、函数或类实例)放入 Redux 存储的 state 或派发的 actions 中。这保证了 Redux DevTools 等调试工具能按预期工作,也保证 UI 能按预期更新。

例外:如果某个 action 会被中间件拦截并在到达 reducer 前终止,则可以在 action 中放置不可序列化的值。诸如 redux-thunk 和 redux-promise 等中间件即为示例。

每个应用只使用一个 Redux Store​

标准 Redux 应用应仅有一个 Redux store 实例,由整个应用共享。一般应在单独文件(如 store.js)中定义。

理想情况下,应用逻辑不直接导入 store,应该通过 <Provider> 传递给 React 组件树,或通过如 thunks 的中间件间接引用。在极少数情况下,可能需要在其他逻辑文件中导入 store,但应尽量避免。

优先级 B 规则:强烈推荐​

使用 Redux Toolkit 编写 Redux 逻辑​

Redux Toolkit 是我们推荐的 Redux 工具集。它内置了我们建议的最佳实践,包括配置 store 以检测状态修改并启用 Redux DevTools 扩展、使用 Immer 简化不可变更新逻辑等。

你不必一定使用 RTK,也可使用其他方法,但使用 RTK 能简化你的逻辑,确保应用默认配置良好。

使用 Immer 编写不可变更新​

手动编写不可变更新逻辑通常很困难,也容易出错。Immer 允许你用更简单的“修改式”逻辑编写不可变更新,甚至会在开发环境冻结状态,以捕获应用其他位置的修改。我们建议使用 Immer 编写不可变更新,最好将它作为 Redux Toolkit 的一部分使用。

使用“功能文件夹+单文件逻辑”结构组织文件​

Redux 本身不关心你如何组织应用文件夹和文件。但将某个功能的逻辑聚集在一起通常更易维护。

因此,我们建议大多数应用使用“功能文件夹”结构(将一个功能的所有文件放在同一文件夹)。在功能文件夹内,应使用单个“slice”文件编写该功能的 Redux 逻辑,最好用 Redux Toolkit 的 createSlice API。(这也称为 "ducks" 模式)。虽然旧的 Redux 代码库多用“按类型划分文件夹”方式(如将 actions、reducers 放在不同文件夹),但集中相关逻辑更易查找和更新。

详细说明:示例文件结构

示例文件结构可能如下所示:

  • /src
    • index.tsx:入口文件,渲染 React 组件树
    • /app
      • store.ts:store 配置
      • rootReducer.ts:根 reducer(可选)
      • App.tsx:根 React 组件
    • /common:hooks、通用组件、工具等
    • /features:包含所有“功能文件夹”
      • /todos:单个功能文件夹
        • todosSlice.ts:Redux reducer 逻辑及关联 actions
        • Todos.tsx:React 组件

/app 包含依赖于其他目录的应用级设置和布局。

/common 包含真正通用且可重用工具和组件。

/features 中的文件夹包含与特定功能相关的所有功能。本例中,todosSlice.ts 是一个“duck”风格文件,调用 RTK 的 createSlice() 函数,导出 slice reducer 和 action 创建器。

尽可能将逻辑放入 Reducers​

尽可能把计算新 state 的大部分逻辑放到相应 reducer 中,而不是在准备和派发 action 的代码里(比如点击处理函数)。这有助于确保更多实际业务逻辑易于测试,支持更有效的时间旅行调试,避免导致修改和错误的常见失误。

有些场景确实需要先行计算新 state(如生成唯一 ID),但应尽量减少。

Detailed Explanation

Redux 核心不关心新 state 是在 reducer 还是在 action 创建逻辑中计算的。比如一个待办事项应用,“切换待办”动作需要不可变地更新待办数组。可以只在 action 中携带待办 ID,在 reducer 中计算新数组:

// Click handler:
const onTodoClicked = (id: string) => {
dispatch(todoToggled(id))
}

// Reducer:
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoToggled(state, action: PayloadAction<string>) {
const todo = state.find(todo => todo.id === action.payload)
if (todo) {
todo.completed = !todo.completed
}
}
}
})

也可以先计算新数组,再将整个新数组放入 action:

// Click handler:
const onTodoClicked = (id: string) => {
const newTodos = todos.map(todo => {
if (todo.id !== id) return todo

return { ...todo, completed: !todo.completed }
})

dispatch(todosReplaced(newTodos))
}

// Reducer:
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todosReplaced(state, action: PayloadAction<Todo[]>) {
return action.payload
}
}
})

但在 reducer 中做逻辑更好,理由有:

  • Reducers 是纯函数,易于测试 —— 只需调用 const result = reducer(testState, action) 并断言结果符合预期。逻辑越多在 reducer,测试覆盖越高。
  • Redux 状态更新必须遵循不可变更新规则。大多数人知道 reducer 内必须遵守,但不明显的是,如果新 state 在 reducer 外部计算,也必须遵守,这容易出错,如意外修改或将 store 里的值读出后原样写回 action。全部状态计算放到 reducer 避免这类错误。
  • 使用 Redux Toolkit 或 Immer,在 reducer 中编写不可变更新更简易,Immer 会冻结状态捕获意外修改。
  • 时间旅行调试是通过“撤销”已派发的 action,再重做或做其他操作实现的。热重载 reducer 也通常是在现有 action 上重新运行 reducer。如果 action 正确而 reducer 有 bug,可以编辑 reducer 修复,热重载后马上得到正确状态;如果 action 错误,则必须重新执行产生该 action 的操作。将更多逻辑放入 reducer 有利于调试。
  • 最后,放在 reducer 里意味着知道在哪里找状态更新逻辑,不会分散在应用代码各处。

Reducers 应该拥有 State 形状​

Redux 根状态由单根 reducer 计算。为维护性考虑,该 reducer 通常拆成按 key/value 切分的“slice”,每个“slice reducer”负责提供该状态片段的初始值并计算更新。

此外,slice reducer 应对返回的状态值保持控制。尽量减少使用“盲目展开/返回”,比如 return action.payload 或 return {...state, ...action.payload},因为这依赖派发 action 的代码的正确格式,等于 reducer 放弃了对状态形态的拥有权,若 action 内容不正确会导致 bug。

注意:对于像表单数据编辑这类场景,写单独的 action 类型来对应每个字段既费时又无多大益处,使用“展开返回”的 reducer 是合理选择。

Detailed Explanation

例如,有个“当前用户” reducer:

const initialState = {
firstName: null,
lastName: null,
age: null
}

const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
userLoggedIn(state, action) {
return action.payload
}
}
})

这里 reducer 直接假设 action.payload 是格式正确的用户对象。

但若某处代码错误地派发了一个“待办”对象,而非用户对象:

dispatch({
type: 'users/userLoggedIn',
payload: {
id: 42,
text: 'Buy milk'
}
})

reducer 盲目返回了该待办对象,导致后续应用读取用户数据时崩溃。

至少可在 reducer 加入部分验证,确保 action.payload 有正确字段,或按名称取字段,虽会增加代码,但提高安全性。

使用静态类型会让此类代码更安全、易接受。如果 reducer 知道 action 是 PayloadAction<User>,那么做 return action.payload 应该是安全的。

根据存储数据命名状态切片​

如 Reducers 应该拥有 State 形状 所述,划分 reducer 逻辑时通常基于状态“切片”,combineReducers 正是用来合并这些 slice reducer。

传给 combineReducers 的对象中的键名决定了返回状态对象中的键名。请确保根据状态数据含义命名这些键名,避免在键名中包含“reducer”。对象应如 {users: {}, posts: {}},而非 {usersReducer: {}, postsReducer: {}}。

Detailed Explanation

对象字面量简写让声明键名和值同时完成变得简单:

const data = 42
const obj = { data }
// 等同于:{data: data}

combineReducers 接收一个 reducer 函数组成的对象,并生成具有相同键名的状态对象。

这导致了常见错误:导入 reducer 时变量名带有 “reducer”,且使用字面量简写传给 combineReducers:

import usersReducer from 'features/users/usersSlice'

const rootReducer = combineReducers({
usersReducer
})

此处 {usersReducer} 实际生成 {usersReducer: usersReducer},使“reducer”成为状态键名的一部分,冗余无用。

应只用键名反映所存数据,建议明确写法:

import usersReducer from 'features/users/usersSlice'
import postsReducer from 'features/posts/postsSlice'

const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer
})

多写几个字符,却生成更易懂的代码和状态定义。

根据数据类型组织状态结构,而非组件​

根状态切片的定义和命名应基于应用的主要数据类型或功能区块,而非特定 UI 组件。由于 Redux store 中的数据与 UI 组件之间不存在严格的 1:1 关系,且许多组件可能使用相同数据。应将状态树视为任意部分 app 可访问的全局数据库,组件只读取自身需要的状态片段。

例如,一个博客应用可能需要跟踪谁登录了、作者和帖子信息、以及当前激活的屏幕信息。合理的状态结构可能是 {auth, posts, users, ui}。糟糕的结构如 {loginScreen, usersList, postsList}。

把 Reducers 看作状态机​

许多 Redux reducer 是“无条件”的,只看派发的 action,计算新状态,而不基于当前状态的上下文。这样容易出错,因为某些动作在特定状态下逻辑上“无效”,例如“请求成功”动作只当状态是“加载中”时才有新状态,或“更新某项”动作只在有“正在编辑”的项目时才应派发。

为避免,应把 reducer 当作“状态机”,以当前状态和派发动作的组合决定是否计算新状态,而不是无条件只基于动作本身。

Detailed Explanation

有限状态机 是一种建模工具,用于表征某事物在任何时刻只处于有限多个“有限状态”之一。例如 fetchUserReducer 可有状态:

  • "idle"(尚未开始获取)
  • "loading"(正在获取用户)
  • "success"(成功获取用户)
  • "failure"(获取失败)

为了明确表示有限状态并让不可能的状态不可能,可以在 state 中指定表示状态的属性:

const initialUserState = {
status: 'idle', // 显式有限状态
user: null,
error: null
}

使用 TypeScript,可方便采用判别式联合类型描述各有限状态。例如,当 state.status === 'success',你可期望 state.user 有值且 state.error 不应为真。类型系统可强制执行此逻辑。

通常,写 reducer 逻辑时优先看 action。但用状态机建模重要的是优先考虑 state。针对每种状态创建“有限状态 reducer”可封装每个状态对应行为:

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

type UserState =
| { status: 'idle'; user: null; error: null }
| { status: 'loading'; user: null; error: null }
| { status: 'success'; user: User; error: null }
| { status: 'failure'; user: null; error: string }

const initialState: UserState = { status: 'idle', user: null, error: null }

const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
fetchUserStarted(state) {
// Only an idle or failed fetch can start loading
switch (state.status) {
case 'idle':
case 'failure':
return { status: 'loading', user: null, error: null }
default:
return state
}
},
fetchUserSucceeded(state, action: PayloadAction<User>) {
// A result only matters if we were actually loading
switch (state.status) {
case 'loading':
return { status: 'success', user: action.payload, error: null }
default:
return state
}
},
fetchUserFailed(state, action: PayloadAction<string>) {
switch (state.status) {
case 'loading':
return { status: 'failure', user: null, error: action.payload }
default:
return state
}
}
}
})

由于现在是针对每种状态定义行为,而不是针对每个 action 定义行为,因此也能避免不可能的状态转换。例如,当 status === 'loading' 时,fetchUserStarted action 不应产生效果;通过这种方式可以明确保证这一点,避免意外引入边缘情况。

规范复杂嵌套/关联数据状态为标准化结构​

许多应用需要在 store 缓存复杂数据,这些数据通常在 API 返回时是嵌套形式,或者不同实体之间有关联(如博客中用户、帖子、评论关系)。

建议以“标准化”形式存储数据。这方便根据 ID 查找条目并单独更新,更有利于性能优化。

保持状态最小化并派生附加值​

尽可能让 Redux store 中保存的实际数据保持最小,只在需要时从状态“派生”附加值。如计算过滤列表或求和。举例,todo 应用在状态中存储原始 todo 数组,过滤后的 todo 列表则在状态外计算。是否所有 todo 完成、剩余数量等也应在状态外计算。

该做法优点:

  • 实际状态易读
  • 计算附加值的逻辑和同步保持较少
  • 原始状态始终可得且不会被替换

派生数据通常通过“selector”函数完成,它可以封装派生计算逻辑。为了提升性能,可以使用 Reselect(Redux Toolkit 以 createSelector 的形式重新导出)对 selector 进行_记忆化_,缓存之前的结果。

将 Actions 视为事件而非设置器(Setters)​

Redux 本身不关心 action.type 内容,只要被定义即可。action 值可用现在时("users/update")、过去时("users/updated")、事件描述("upload/progress")、或当做设置器("users/setUserName")等。你自己决定动作在应用中的含义及建模方式。

然而,我们推荐尽量把动作视为“描述发生的事件”,而非“设置器”。视为“事件”通常导致更有意义的动作名称、更少动作派发与更有价值的动作日志。写“设置器”往往导致动作类型过多、派发次数太多,且动作日志不具备实质意义。

Detailed Explanation

假设你写餐厅应用,顾客点了一个披萨和一瓶可乐。你可以派发:

{ type: "food/orderAdded", payload: {pizza: 1, coke: 1} }

或者派发:

{
type: "orders/setPizzasOrdered",
payload: {
amount: getState().orders.pizza + 1,
}
}

{
type: "orders/setCokesOrdered",
payload: {
amount: getState().orders.coke + 1,
}
}

第一例是“事件”,意为“有人点了披萨和可乐,请处理”。

第二例则是“设置器”,意为“我知道有披萨和可乐的字段,命令你设置当前值”。

“事件”只需派发一条动作,更灵活。不管之前点了多少披萨,可能当时没厨师,订单被忽略。

“设置器”要求客户端知道状态结构及正确值,得派发多条动作完成“事务”。

编写有意义的 Action 名称​

action.type 有两个主要作用:

  • reducer 根据类型判断是否处理该动作以计算新状态
  • Redux DevTools 历史日志显示动作类型供开发者查看

如 将动作建模为“事件” 所述,Redux 不关心 type 内容,但对开发者而言非常重要。应使用有意义、信息丰富且描述清晰的类型字段。理想情况下,浏览 dispatched 的动作类型列表时,无需查看动作具体内容即可理解应用发生了什么。避免使用诸如 "SET_DATA"、"UPDATE_STORE" 这类过于笼统的名称。

允许多个 Reducers 响应同一 Action​

Redux reducer 逻辑预期拆成很多小 reducer,各自独立更新状态树的对应部分,最终合并成根 reducer。当某动作派发时,可由所有、部分或无 reducer 处理。

你应当允许多个 reducer 分别响应相同动作。实际经验表明,大多数动作通常只被单一 reducer 处理,这无问题。但把动作视作“事件”,允许多个 reducer 响应,有助代码库规模扩张,减少派发多个动作才能完成一项业务的次数。

避免连续派发大量动作​

避免为完成较大“事务”而连续派发许多动作。此做法虽合法,但通常导致多次较昂贵的 UI 更新,且某些中间状态可能被别处逻辑视为无效。优先派发单条“事件”类动作以完成所有状态更新,或考虑使用动作合批插件以单次 UI 更新派发多个动作。

Detailed Explanation

无数量限制,可连续多次派发动作。但每次派发都会触发所有 store 订阅回调(通常一个或多个连接的 UI 组件),通常伴随 UI 更新。

React 事件处理器中的更新会批量渲染,但事件处理器外触发的更新不会(如异步函数、定时器回调、非 React 代码中的 dispatch)。此时每次 dispatch 会同步执行完整 React 渲染,影响性能。

此外,多次派发的动作组成的“事务”在中间路径将产生不完整状态。例如同时派发 "UPDATE_A"、"UPDATE_B"、"UPDATE_C",若某代码期望三者同时更新,则前两次派发后状态不完整。

如果确实需要多次 dispatch,可以考虑以某种方式批处理更新。React 18 及更高版本已经会将同一事件周期中的多次 dispatch 合并为一次渲染,因此剩下的选择是对 store 通知回调进行防抖,或将多个 action 组合为一次 dispatch,从而只通知订阅者一次。更多示例及相关扩展链接请参阅如何减少 store 更新事件常见问题。

评估每个状态片段应该存放位置​

Redux 三大原则说“整个应用的状态存储在单一树中”,该说法已被过度解读。这并不意味着所有值都必须存在 Redux store,而是应有一个单一位置存储所有你认为的全局、应用范围内的状态。局部值一般应放在最近的 UI 组件中。

因此,开发者应自行决定哪些状态应放在 Redux store,哪些应保留在组件状态。参考此规则帮助评估每个状态并确定存放位置。

使用 React-Redux Hooks API​

优先使用 React-Redux hooks API(useSelector 和 useDispatch),作为 React 组件与 Redux store 交互的默认方式。经典的 connect API 仍可正常工作,也会继续受支持,但 hooks API 通常更易使用:间接层更少、需要编写的代码更少,并且比 connect 更容易与 TypeScript 配合。

hooks API 在性能和数据流上与 connect 有不同权衡,但我们现推荐其作为默认选择。

Detailed Explanation

经典的 connect API 是一个高阶组件。它会生成新的包装组件,订阅 store、渲染你的组件,并将 store 数据和 action creator 作为 props 传入。

这是有意设计的间接层,便于写无特定 Redux 依赖的“展示组件”。

Hooks 改变了大部分 React 开发者的写法。虽然“容器/展示组件”概念仍有用,但 hooks 鼓励组件自身通过调用 hook 获取数据,导致编写和测试组件及逻辑的方式不同。

connect 的间接性让部分用户难理解数据流。此外,connect 的复杂性也使其在 TypeScript 下难以类型化,因其存在多重重载、可选参数、mapState/mapDispatch/父组件 props 合并、绑定 action 创建器和 thunk 等。

useSelector 和 useDispatch 去除了间接,组件与 Redux 交互更清晰。useSelector 仅接收单个选择器,使用 TypeScript 更简单,useDispatch 同理。

更多细节请参考 Redux 主维护者 Mark Erikson 关于 hooks 与高阶组件权衡的博文和演讲:

有关如何正确优化组件及处理少见边缘情况,也请参阅 React-Redux hooks API 文档。

连接更多组件从 Store 读取数据​

建议让更多 UI 组件订阅 Redux store,按更细粒度读取数据。这通常提升 UI 性能,因为状态变化时需重新渲染的组件变少。

例如,不要让 <UserList> 组件读取整个用户数组,而应让它选择所有用户 ID 列表并将条目渲染为 <UserListItem userId={userId}>;然后由 <UserListItem> 调用 useSelector,从 store 中提取自己的用户条目。

这既适用于 useSelector() hook,也适用于旧版 connect() API。

使用 connect 时,mapDispatch 用对象简写形式​

此规则仅适用于仍在使用旧版 connect API 的情况。使用 hooks 编写的组件应直接调用 useDispatch。

connect 的 mapDispatch 参数可以是接收 dispatch 的函数,也可以是包含 action creator 的对象。我们建议始终使用 mapDispatch 的“对象简写”形式,因为它能大幅简化代码。几乎没有必要把 mapDispatch 写成函数。

在函数组件中多次调用 useSelector​

使用 useSelector 读取数据时,优先多次调用 useSelector 拿小块数据,而非单次调用返回多个结果的对象。useSelector 不要求返回对象,小数据选择器减少了状态变化导致组件渲染的概率。

不过请找到合适的粒度平衡。若单组件确实需要一整个状态切片,写一个返回整个切片的 useSelector 比为每个字段写多个选择器更好。

使用静态类型​

建议使用 TypeScript 等静态类型系统,而不是纯 JavaScript。类型系统可以捕获许多常见错误、改善代码的文档性,并最终提高长期可维护性。虽然 Redux 和 React-Redux 最初是为纯 JS 设计的,但都能很好地配合 TS 使用。Redux Toolkit 本身使用 TS 编写,设计目标是在只需少量额外类型声明的情况下提供良好的类型安全。

使用 Redux DevTools 扩展调试​

配置 Redux store 以启用 Redux DevTools Extension 调试。该工具允许你查看:

此外,DevTools 支持时间旅行调试,允许你在动作历史中前后切换,查看不同时刻的应用状态和 UI。

Redux 就是为支持此类调试设计的,DevTools 是使用 Redux 的最强大理由之一。

状态树应使用普通 JS 对象​

建议状态树使用普通 JS 对象和数组,而不是像 Immutable.js 这类专门库。虽然使用 Immutable.js 有一些潜在好处,但常说的轻量级引用比较及高效更新是不可变更新的普遍特性,不必特定库支持。这样可减小包体积,减少数据类型转换引入的复杂性。

如前所述,若想简化不可变更新逻辑,特别推荐将 Immer 作为 Redux Toolkit 的一部分使用。

Detailed Explanation

Immutable.js 自 Redux 诞生以来偶尔用于 Redux 应用,原因多为:

  • 利用廉价引用比较提升性能
  • 利用特殊数据结构加快更新
  • 避免意外修改
  • 方便嵌套更新,如 setIn() API

这些理由中有部分合理,但实践中收益不及预期,且存在问题:

  • 廉价引用比较是所有不可变更新的属性,不唯 Immutable.js 独有
  • 可通过 Immer 或 redux-immutable-state-invariant 等其它机制防止意外修改
  • Immer 简化整体更新逻辑,取代了 setIn() 的必要
  • Immutable.js 体积庞大
  • API 复杂
  • API 影响应用代码风格,程序需区分处理普通对象与 Immutable 对象
  • 转换 Immutable 到普通对象代价高且会产生全新深层对象引用
  • 维护活跃度不足

唯一保留的有力理由是非常大体量对象的快速更新(数万个键)。大多数应用不会涉及如此庞大对象。

综上,Immutable.js 造成的负担大于实际收益。Immer 是更好选择。