Skip to main content

Redux 风格指南

¥Redux Style Guide

介绍

¥Introduction

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

¥This is the official style guide for writing Redux code. It lists our recommended patterns, best practices, and suggested approaches for writing Redux applications.

Redux 核心库和大多数 Redux 文档都是不带偏见的。使用 Redux 的方法有很多种,而且很多时候没有单一的 "right" 方法来做事情。

¥Both the Redux core library and most of the Redux documentation are unopinionated. There are many ways to use Redux, and much of the time there is no single "right" way to do things.

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

¥However, time and experience have shown that for some topics, certain approaches work better than others. In addition, many developers have asked us to provide official guidance to reduce decision fatigue.

考虑到这一点,我们整理了这份建议列表,以帮助你避免错误、自行车脱落和反模式。我们还了解团队偏好各不相同,不同的项目有不同的要求,因此没有适合所有规模的风格指南。我们鼓励你遵循这些建议,但请花时间评估你自己的情况并决定它们是否适合你的需求。

¥With that in mind, we've put together this list of recommendations to help you avoid errors, bikeshedding, and anti-patterns. We also understand that team preferences vary and different projects have different requirements, so no style guide will fit all sizes. You are encouraged to follow these recommendations, but take the time to evaluate your own situation and decide if they fit your needs.

最后,我们要感谢 Vue 文档作者编写了 Vue 风格指南页面,这是本页面的灵感来源。

¥Finally, we'd like to thank the Vue documentation authors for writing the Vue Style Guide page, which was the inspiration for this page.

规则类别

¥Rule Categories

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

¥We've divided these rules into three categories:

优先级 A:基础

¥Priority A: Essential

这些规则有助于防止错误,因此请不惜一切代价学习并遵守它们。例外情况可能存在,但应该非常罕见,并且只有那些具有 JavaScript 和 Redux 专业知识的人才能做出。

¥These rules help prevent errors, so learn and abide by them at all costs. Exceptions may exist, but should be very rare and only be made by those with expert knowledge of both JavaScript and Redux.

¥Priority B: Strongly Recommended

人们发现这些规则可以提高大多数项目的可读性和/或开发者体验。如果你违反了这些规定,你的代码仍然会运行,但违规行为应该很少见且有充分理由。只要合理可能,请遵守这些规则。

¥These rules have been found to improve readability and/or developer experience in most projects. Your code will still run if you violate them, but violations should be rare and well-justified. Follow these rules whenever it is reasonably possible.

¥Priority C: Recommended

当存在多个同样好的选项时,可以进行任意选择以确保一致性。在这些规则中,我们描述了每个可接受的选项并建议默认选择。这意味着你可以随意在自己的代码库中做出不同的选择,只要你保持一致并且有充分的理由。但请一定要有充分的理由!

¥Where multiple, equally good options exist, an arbitrary choice can be made to ensure consistency. In these rules, we describe each acceptable option and suggest a default choice. That means you can feel free to make a different choice in your own codebase, as long as you're consistent and have a good reason. Please do have a good reason though!

优先级 A 规则:基础

¥Priority A Rules: Essential

不要改变状态

¥Do Not Mutate State

状态突变是 Redux 应用中出现错误的最常见原因,包括组件无法正确重新渲染,并且还会破坏 Redux DevTools 中的时间旅行调试。无论是在 reducer 内部还是在所有其他应用代码中,都应始终避免状态值的实际突变。

¥Mutating state is the most common cause of bugs in Redux applications, including components failing to re-render properly, and will also break time-travel debugging in the Redux DevTools. Actual mutation of state values should always be avoided, both inside reducers and in all other application code.

使用 redux-immutable-state-invariant 等工具来捕获开发过程中的突变,使用 伊梅尔 来避免状态更新中的意外突变。

¥Use tools such as redux-immutable-state-invariant to catch mutations during development, and Immer to avoid accidental mutations in state updates.

注意:可以修改现有值的副本 - 这是编写不可变更新逻辑的正常部分。另外,如果你使用 Immer 库进行不可变更新,则编写 "mutating" 逻辑是可以接受的,因为真实数据不会发生突变 - Immer 安全地跟踪更改并在内部生成不可变更新的值。

¥Note: it is okay to modify copies of existing values - that is a normal part of writing immutable update logic. Also, if you are using the Immer library for immutable updates, writing "mutating" logic is acceptable because the real data isn't being mutated - Immer safely tracks changes and generates immutably-updated values internally.

reducer 不能有副作用

¥Reducers Must Not Have Side Effects

Reducer 函数应该只依赖于它们的 stateaction 参数,并且应该只根据这些参数计算并返回一个新的状态值。它们不得执行任何类型的异步逻辑(AJAX 调用、超时、promise)、生成随机值(Date.now()Math.random())、修改 reducer 外部的变量或运行影响 reducer 函数范围之外的其他代码。

¥Reducer functions should only depend on their state and action arguments, and should only calculate and return a new state value based on those arguments. They must not execute any kind of asynchronous logic (AJAX calls, timeouts, promises), generate random values (Date.now(), Math.random()), modify variables outside the reducer, or run other code that affects things outside the scope of the reducer function.

注意:让 reducer 调用在其外部定义的其他函数是可以接受的,例如从库或实用程序函数中导入,只要它们遵循相同的规则即可。

¥Note: It is acceptable to have a reducer call other functions that are defined outside of itself, such as imports from libraries or utility functions, as long as they follow the same rules.

Detailed Explanation

该规则的目的是保证 reducer 在调用时的行为是可预测的。例如,如果你正在进行时间旅行调试,则可能会使用较早的操作多次调用 reducer 函数以生成 "current" 状态值。如果 reducer 有副作用,这将导致这些效果在调试过程中被执行,并导致应用以意想不到的方式运行。

¥The purpose of this rule is to guarantee that reducers will behave predictably when called. For example, if you are doing time-travel debugging, reducer functions may be called many times with earlier actions to produce the "current" state value. If a reducer has side effects, this would cause those effects to be executed during the debugging process, and result in the application behaving in unexpected ways.

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

¥There are some gray areas to this rule. Strictly speaking, code such as console.log(state) is a side effect, but in practice has no effect on how the application behaves.

不要将不可序列化的值放入状态或操作中

¥Do Not Put Non-Serializable Values in State or Actions

避免将不可序列化的值(例如 Promises、Symbols、Maps/Sets、函数或类实例)放入 Redux 存储状态或分派操作中。这确保了通过 Redux DevTools 进行调试等功能将按预期工作。它还确保 UI 将按预期更新。

¥Avoid putting non-serializable values such as Promises, Symbols, Maps/Sets, functions, or class instances into the Redux store state or dispatched actions. This ensures that capabilities such as debugging via the Redux DevTools will work as expected. It also ensures that the UI will update as expected.

例外:如果操作在到达 reducer 之前被中间件拦截并停止,则可以在操作中放置不可序列化的值。redux-thunkredux-promise 等中间件就是这样的例子。

¥Exception: you may put non-serializable values in actions if the action will be intercepted and stopped by a middleware before it reaches the reducers. Middleware such as redux-thunk and redux-promise are examples of this.

每个应用只有一个 Redux Store

¥Only One Redux Store Per App

标准 Redux 应用应该只有一个 Redux 存储实例,该实例将由整个应用使用。它通常应该在单独的文件中定义,例如 store.js

¥A standard Redux application should only have a single Redux store instance, which will be used by the whole application. It should typically be defined in a separate file such as store.js.

理想情况下,应用逻辑不会直接导入存储。它应该通过 <Provider> 传递到 React 组件树,或者通过中间件(例如 thunk)间接引用。在极少数情况下,你可能需要将其导入到其他逻辑文件中,但这应该是最后的手段。

¥Ideally, no app logic will import the store directly. It should be passed to a React component tree via <Provider>, or referenced indirectly via middleware such as thunks. In rare cases, you may need to import it into other logic files, but this should be a last resort.

¥Priority B Rules: Strongly Recommended

使用 Redux Toolkit 编写 Redux 逻辑

¥Use Redux Toolkit for Writing Redux Logic

Redux 工具包 是我们推荐的使用 Redux 的工具集。它具有构建在我们建议的最佳实践中的功能,包括设置存储以捕获突变并启用 Redux DevTools 扩展、使用 Immer 简化不可变更新逻辑等等。

¥Redux Toolkit is our recommended toolset for using Redux. It has functions that build in our suggested best practices, including setting up the store to catch mutations and enable the Redux DevTools Extension, simplifying immutable update logic with Immer, and more.

你不需要将 RTK 与 Redux 结合使用,并且如果需要,你可以自由使用其他方法,但使用 RTK 将简化你的逻辑并确保你的应用设置良好的默认值。

¥You are not required to use RTK with Redux, and you are free to use other approaches if desired, but using RTK will simplify your logic and ensure that your application is set up with good defaults.

使用 Immer 编写不可变更新

¥Use Immer for Writing Immutable Updates

手动编写不可变的更新逻辑通常很困难并且容易出错。伊梅尔 允许你使用 "mutative" 逻辑编写更简单的不可变更新,甚至冻结开发中的状态以捕获应用中其他位置的突变。我们建议使用 Immer 来编写不可变的更新逻辑,最好作为 Redux 工具包 的一部分。

¥Writing immutable update logic by hand is frequently difficult and prone to errors. Immer allows you to write simpler immutable updates using "mutative" logic, and even freezes your state in development to catch mutations elsewhere in the app. We recommend using Immer for writing immutable update logic, preferably as part of Redux Toolkit.

使用单文件逻辑将文件构造为功能文件夹

¥Structure Files as Feature Folders with Single-File Logic

Redux 本身并不关心应用的文件夹和文件的结构。但是,将给定功能的逻辑共同定位在一个位置通常可以更轻松地维护该代码。

¥Redux itself does not care about how your application's folders and files are structured. However, co-locating logic for a given feature in one place typically makes it easier to maintain that code.

因此,我们建议大多数应用应使用 "功能文件夹" 方法构建文件(同一文件夹中某个功能的所有文件)。在给定的功能文件夹中,该功能的 Redux 逻辑应编写为单个 "slice" 文件,最好使用 Redux Toolkit createSlice API。(这也称为 "ducks" 图案)。虽然较旧的 Redux 代码库通常使用 "folder-by-type" 方法,并为 "actions" 和 "reducers" 提供单独的文件夹,但将相关逻辑放在一起可以更轻松地查找和更新该代码。

¥Because of this, we recommend that most applications should structure files using a "feature folder" approach (all files for a feature in the same folder). Within a given feature folder, the Redux logic for that feature should be written as a single "slice" file, preferably using the Redux Toolkit createSlice API. (This is also known as the "ducks" pattern). While older Redux codebases often used a "folder-by-type" approach with separate folders for "actions" and "reducers", keeping related logic together makes it easier to find and update that code.

Detailed Explanation: Example Folder Structure

示例文件夹结构可能类似于:

¥An example folder structure might look something like:

  • /src

    • index.tsx:渲染 React 组件树的入口点文件

      ¥index.tsx: Entry point file that renders the React component tree

    • /app

      • store.ts:存储设置

        ¥store.ts: store setup

      • rootReducer.ts:根部 reducer(可选)

        ¥rootReducer.ts: root reducer (optional)

      • App.tsx:根反应组件

        ¥App.tsx: root React component

    • /common:钩子、通用组件、实用程序等

      ¥/common: hooks, generic components, utils, etc

    • /features:包含全部 "功能文件夹"

      ¥/features: contains all "feature folders"

      • /todos:单个功能文件夹

        ¥/todos: a single feature folder

        • todosSlice.ts:Redux reducer 逻辑和相关操作

          ¥todosSlice.ts: Redux reducer logic and associated actions

        • Todos.tsx:一个反应组件

          ¥Todos.tsx: a React component

/app 包含依赖于所有其他文件夹的应用范围的设置和布局。

¥/app contains app-wide setup and layout that depends on all the other folders.

/common 包含真正通用且可重用的实用程序和组件。

¥/common contains truly generic and reusable utilities and components.

/features 的文件夹包含与特定功能相关的所有功能。在此示例中,todosSlice.ts 是 "duck" 样式的文件,其中包含对 RTK 的 createSlice() 函数的调用,并导出切片缩减器和动作创建器。

¥/features has folders that contain all functionality related to a specific feature. In this example, todosSlice.ts is a "duck"-style file that contains a call to RTK's createSlice() function, and exports the slice reducer and action creators.

将尽可能多的逻辑放入 Reducers 中

¥Put as Much Logic as Possible in Reducers

只要有可能,尝试将尽可能多的用于计算新状态的逻辑放入适当的 reducer 中,而不是放在准备和分派操作的代码中(如单击处理程序)。这有助于确保更多的实际应用逻辑易于测试,能够更有效地使用时间旅行调试,并有助于避免可能导致突变和错误的常见错误。

¥Wherever possible, try to put as much of the logic for calculating a new state into the appropriate reducer, rather than in the code that prepares and dispatches the action (like a click handler). This helps ensure that more of the actual app logic is easily testable, enables more effective use of time-travel debugging, and helps avoid common mistakes that can lead to mutations and bugs.

在某些有效情况下,应首先计算部分或全部新状态(例如生成唯一 ID),但应将其保持在最低限度。

¥There are valid cases where some or all of the new state should be calculated first (such as generating a unique ID), but that should be kept to a minimum.

Detailed Explanation

Redux 核心实际上并不关心新的状态值是在 reducer 中还是在 action 创建逻辑中计算的。例如,对于待办事项应用,"切换待办事项" 操作的逻辑需要不可变地更新待办事项数组。让操作仅包含待办事项 ID 并在 reducer 中计算新数组是合法的:

¥The Redux core does not actually care whether a new state value is calculated in the reducer or in the action creation logic. For example, for a todo app, the logic for a "toggle todo" action requires immutably updating an array of todos. It is legal to have the action contain just the todo ID and calculate the new array in the reducer:

// Click handler:
const onTodoClicked = (id) => {
dispatch({type: "todos/toggleTodo", payload: {id}})
}

// Reducer:
case "todos/toggleTodo": {
return state.map(todo => {
if(todo.id !== action.payload.id) return todo;

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

并且首先计算新数组并将整个新数组放入操作中:

¥And also to calculate the new array first and put the entire new array in the action:

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

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

dispatch({ type: 'todos/toggleTodo', payload: { todos: newTodos } })
}

// Reducer:
case "todos/toggleTodo":
return action.payload.todos;

然而,由于以下几个原因,在 reducer 中执行逻辑是更好的选择:

¥However, doing the logic in the reducer is preferable for several reasons:

  • reducer 总是很容易测试,因为它们是纯函数 - 你只需调用 const result = reducer(testState, action),并断言结果就是你所期望的。因此,你可以在 reducer 中放入的逻辑越多,你拥有的易于测试的逻辑就越多。

    ¥Reducers are always easy to test, because they are pure functions - you just call const result = reducer(testState, action), and assert that the result is what you expected. So, the more logic you can put in a reducer, the more logic you have that is easily testable.

  • Redux 状态更新必须始终遵循 不可变更新规则。大多数 Redux 用户意识到他们必须遵循 reducer 内部的规则,但如果新状态是在 reducer 外部计算的,那么你是否也必须这样做并不明显。这很容易导致错误,例如意外突变,甚至从 Redux 存储中读取值并将其直接传递回操作中。在 reducer 中进行所有状态计算可以避免这些错误。

    ¥Redux state updates must always follow the rules of immutable updates. Most Redux users realize they have to follow the rules inside a reducer, but it's not obvious that you also have to do this if the new state is calculated outside the reducer. This can easily lead to mistakes like accidental mutations, or even reading a value from the Redux store and passing it right back inside an action. Doing all of the state calculations in a reducer avoids those mistakes.

  • 如果你使用 Redux Toolkit 或 Immer,则在 reducer 中编写不可变更新逻辑要容易得多,并且 Immer 将冻结状态并捕获意外突变。

    ¥If you are using Redux Toolkit or Immer, it is much easier to write immutable update logic in reducers, and Immer will freeze the state and catch accidental mutations.

  • 时间旅行调试的工作原理是让你 "undo" 一个已调度的操作,然后执行不同的操作或 "redo" 该操作。此外,reducer 的热重载通常涉及使用现有操作重新运行新 reducer。如果你有正确的操作,但 reducer 有问题,你可以编辑 reducer 来修复错误,热重载它,你应该立即获得正确的状态。如果操作本身错误,你必须重新运行导致调度该操作的步骤。因此,如果 reducer 中有更多逻辑,则调试起来会更容易。

    ¥Time-travel debugging works by letting you "undo" a dispatched action, then either do something different or "redo" the action. In addition, hot-reloading of reducers normally involves re-running the new reducer with the existing actions. If you have a correct action but a buggy reducer, you can edit the reducer to fix the bug, hot-reload it, and you should get the correct state right away. If the action itself was wrong, you'd have to re-run the steps that led to that action being dispatched. So, it's easier to debug if more logic is in the reducer.

  • 最后,将逻辑放入 reducer 中意味着你知道在哪里寻找更新逻辑,而不是将其分散在应用代码的随机其他部分中。

    ¥Finally, putting logic in reducers means you know where to look for the update logic, instead of having it scattered in random other parts of the application code.

reducer 应该拥有状态形状

¥Reducers Should Own the State Shape

Redux 根状态由单根 reducer 函数拥有和计算。为了可维护性,该 reducer 旨在按键/值 "切片" 进行拆分,每个 "切片 reducer" 负责提供初始值并计算对该状态切片的更新。

¥The Redux root state is owned and calculated by the single root reducer function. For maintainability, that reducer is intended to be split up by key/value "slices", with each "slice reducer" being responsible for providing an initial value and calculating the updates to that slice of the state.

此外,切片缩减器应该控制作为计算状态的一部分返回的其他值。尽量减少 "盲目展开/返回" 的使用,例如 return action.payloadreturn {...state, ...action.payload},因为它们依赖于分派操作的代码来正确格式化内容,并且 reducer 实际上放弃了对该状态的所有权。如果操作内容不正确,可能会导致错误。

¥In addition, slice reducers should exercise control over what other values are returned as part of the calculated state. Minimize the use of "blind spreads/returns" like return action.payload or return {...state, ...action.payload}, because those rely on the code that dispatched the action to correctly format the contents, and the reducer effectively gives up its ownership of what that state looks like. That can lead to bugs if the action contents are not correct.

注意:对于像在表单中编辑数据这样的场景,"展开返回" 化简器可能是一个合理的选择,在这种情况下,为每个单独的字段编写单独的操作类型将非常耗时且没有什么好处。

¥Note: A "spread return" reducer may be a reasonable choice for scenarios like editing data in a form, where writing a separate action type for each individual field would be time-consuming and of little benefit.

Detailed Explanation

想象一下 "当前用户" reducer,如下所示:

¥Picture a "current user" reducer that looks like:

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

export default usersReducer = (state = initialState, action) {
switch(action.type) {
case "users/userLoggedIn": {
return action.payload;
}
default: return state;
}
}

在这个例子中,reducer 完全假设 action.payload 将是一个格式正确的对象。

¥In this example, the reducer completely assumes that action.payload is going to be a correctly formatted object.

但是,想象一下如果代码的某些部分要在操作内分派 "todo" 对象,而不是 "user" 对象:

¥However, imagine if some part of the code were to dispatch a "todo" object inside the action, instead of a "user" object:

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

reducer 会盲目地返回待办事项,现在当应用尝试从存储读取用户时,应用的其余部分可能会崩溃。

¥The reducer would blindly return the todo, and now the rest of the app would likely break when it tries to read the user from the store.

如果 reducer 进行一些验证检查以确保 action.payload 实际上具有正确的字段,或者尝试按名称读取正确的字段,则这至少可以部分修复。不过,这确实增加了更多代码,因此这是一个为了安全而牺牲更多代码的问题。

¥This could be at least partly fixed if the reducer has some validation checks to ensure that action.payload actually has the right fields, or tries to read the right fields out by name. That does add more code, though, so it's a question of trading off more code for safety.

使用静态类型确实使这种代码更安全并且更容易被接受。如果 reducer 知道 actionPayloadAction<User>,那么执行 return action.payload 应该是安全的。

¥Use of static typing does make this kind of code safer and somewhat more acceptable. If the reducer knows that action is a PayloadAction<User>, then it should be safe to do return action.payload.

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

¥Name State Slices Based On the Stored Data

正如 reducer 应该拥有状态形状 中提到的,拆分 reducer 逻辑的标准方法是基于状态的 "切片"。相应地,combineReducers 是将这些切片 reducer 连接成更大的 reducer 函数的标准函数。

¥As mentioned in Reducers Should Own the State Shape, the standard approach for splitting reducer logic is based on "slices" of state. Correspondingly, combineReducers is the standard function for joining those slice reducers into a larger reducer function.

传递给 combineReducers 的对象中的键名称将定义结果状态对象中的键名称。请务必根据内部保存的数据来命名这些键,并避免在键名称中使用单词 "reducer"。你的对象应该看起来像 {users: {}, posts: {}},而不是 {usersReducer: {}, postsReducer: {}}

¥The key names in the object passed to combineReducers will define the names of the keys in the resulting state object. Be sure to name these keys after the data that is kept inside, and avoid use of the word "reducer" in the key names. Your object should look like {users: {}, posts: {}}, rather than {usersReducer: {}, postsReducer: {}}.

Detailed Explanation

对象字面量速记可以轻松地同时定义对象中的键名和值:

¥Object literal shorthand makes it easy to define a key name and a value in an object at the same time:

const data = 42
const obj = { data }
// same as: {data: data}

combineReducers 接受一个充满 reducer 函数的对象,并使用它来生成具有相同键名称的状态对象。这意味着函数对象中的键名称定义了状态对象中的键名称。

¥combineReducers accepts an object full of reducer functions, and uses that to generate state objects that have the same key names. This means that the key names in the functions object define the key names in the state object.

这会导致一个常见的错误,即使用变量名中的 "reducer" 导入化简器,然后使用对象字面量简写传递给 combineReducers

¥This results in a common mistake, where a reducer is imported using "reducer" in the variable name, and then passed to combineReducers using the object literal shorthand:

import usersReducer from 'features/users/usersSlice'

const rootReducer = combineReducers({
usersReducer
})

在本例中,使用对象字面量速记创建了一个像 {usersReducer: usersReducer} 这样的对象。因此,"reducer" 现在是状态键名称。这是多余且无用的。

¥In this case, use of the object literal shorthand created an object like {usersReducer: usersReducer}. So, "reducer" is now in the state key name. This is redundant and useless.

相反,定义仅与内部数据相关的键名称。为了清楚起见,我们建议使用显式 key: value 语法:

¥Instead, define key names that only relate to the data inside. We suggest using explicit key: value syntax for clarity:

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

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

虽然输入的内容较多,但它会产生最容易理解的代码和状态定义。

¥It's a bit more typing, but it results in the most understandable code and state definition.

根据数据类型而不是组件组织状态结构

¥Organize State Structure Based on Data Types, Not Components

根状态切片应根据应用中的主要数据类型或功能区域来定义和命名,而不是根据 UI 中的特定组件来定义和命名。这是因为 Redux 存储中的数据和 UI 中的组件之间不存在严格的 1:1 关联,并且许多组件可能需要访问相同的数据。将状态树视为一种全局数据库,应用的任何部分都可以访问该数据库以仅读取该组件所需的状态片段。

¥Root state slices should be defined and named based on the major data types or areas of functionality in your application, not based on which specific components you have in your UI. This is because there is not a strict 1:1 correlation between data in the Redux store and components in the UI, and many components may need to access the same data. Think of the state tree as a sort of global database that any part of the app can access to read just the pieces of state needed in that component.

例如,博客应用可能需要跟踪登录者、作者和帖子的信息,以及有关哪个屏幕处于活动状态的一些信息。一个好的状态结构可能看起来像 {auth, posts, users, ui}。糟糕的结构类似于 {loginScreen, usersList, postsList}

¥For example, a blogging app might need to track who is logged in, information on authors and posts, and perhaps some info on what screen is active. A good state structure might look like {auth, posts, users, ui}. A bad structure would be something like {loginScreen, usersList, postsList}.

将 Reducers 视为状态机

¥Treat Reducers as State Machines

许多 Redux reducer 都写成 "unconditionally"。他们只查看分派的操作并计算新的状态值,而不将任何逻辑基于当前状态可能是什么。这可能会导致错误,因为某些操作在某些时候可能在概念上不是 "valid",具体取决于应用逻辑的其余部分。例如,只有当状态表明它已经是 "loading" 时,才应计算 "请求成功" 操作的新值,或者只有在存在标记为 "正在编辑中" 的项目时才应调度 "更新此项目" 操作。

¥Many Redux reducers are written "unconditionally". They only look at the dispatched action and calculate a new state value, without basing any of the logic on what the current state might be. This can cause bugs, as some actions may not be "valid" conceptually at certain times depending on the rest of the app logic. For example, a "request succeeded" action should only have a new value calculated if the state says that it's already "loading", or an "update this item" action should only be dispatched if there is an item marked as "being edited".

为了解决这个问题,请将 reducer 视为 "状态机",其中当前状态和分派的操作的组合决定是否实际计算新的状态值,而不仅仅是无条件地计算操作本身。

¥To fix this, treat reducers as "state machines", where the combination of both the current state and the dispatched action determines whether a new state value is actually calculated, not just the action itself unconditionally.

Detailed Explanation

有限状态机 是一种对在任何时候都只能出现在有限数量的 "有限状态" 中的事物进行建模的有用方法。例如,如果你有 fetchUserReducer,则有限状态可以是:

¥A finite state machine is a useful way of modeling something that should only be in one of a finite number of "finite states" at any time. For example, if you have a fetchUserReducer, the finite states can be:

  • "idle"(尚未开始获取)

    ¥"idle" (fetching not started yet)

  • "loading"(当前正在获取用户)

    ¥"loading" (currently fetching the user)

  • "success"(用户获取成功)

    ¥"success" (user fetched successfully)

  • "failure"(用户获取失败)

    ¥"failure" (user failed to fetch)

为了使这些有限状态清晰且 让不可能的状态变得不可能,你可以指定一个保存此有限状态的属性:

¥To make these finite states clear and make impossible states impossible, you can specify a property that holds this finite state:

const initialUserState = {
status: 'idle', // explicit finite state
user: null,
error: null
}

通过 TypeScript,这也使得使用 受歧视的联合 来表示每个有限状态变得容易。例如,如果 state.status === 'success',那么你会期望 state.user 被定义,但不会期望 state.error 为真。你可以使用类型强制执行此操作。

¥With TypeScript, this also makes it easy to use discriminated unions to represent each finite state. For instance, if state.status === 'success', then you would expect state.user to be defined and wouldn't expect state.error to be truthy. You can enforce this with types.

通常,Reducer 逻辑是通过首先考虑操作来编写的。当使用状态机对逻辑进行建模时,首先考虑状态非常重要。为每个状态创建 "有限状态 reducer" 有助于封装每个状态的行为:

¥Typically, reducer logic is written by taking the action into account first. When modeling logic with state machines, it's important to take the state into account first. Creating "finite state reducers" for each state helps encapsulate behavior per state:

import {
FETCH_USER,
// ...
} from './actions'

const IDLE_STATUS = 'idle';
const LOADING_STATUS = 'loading';
const SUCCESS_STATUS = 'success';
const FAILURE_STATUS = 'failure';

const fetchIdleUserReducer = (state, action) => {
// state.status is "idle"
switch (action.type) {
case FETCH_USER:
return {
...state,
status: LOADING_STATUS
}
}
default:
return state;
}
}

// ... other reducers

const fetchUserReducer = (state, action) => {
switch (state.status) {
case IDLE_STATUS:
return fetchIdleUserReducer(state, action);
case LOADING_STATUS:
return fetchLoadingUserReducer(state, action);
case SUCCESS_STATUS:
return fetchSuccessUserReducer(state, action);
case FAILURE_STATUS:
return fetchFailureUserReducer(state, action);
default:
// this should never be reached
return state;
}
}

现在,由于你是按状态而不是按操作定义行为,因此还可以防止不可能的转换。例如,FETCH_USER 操作在 status === LOADING_STATUS 时应该无效,你可以强制执行该操作,而不是意外引入边缘情况。

¥Now, since you're defining behavior per state instead of per action, you also prevent impossible transitions. For instance, a FETCH_USER action should have no effect when status === LOADING_STATUS, and you can enforce that, instead of accidentally introducing edge-cases.

规范化复杂的嵌套/关系状态

¥Normalize Complex Nested/Relational State

许多应用需要在存储中缓存复杂的数据。该数据通常以嵌套形式从 API 接收,或者在数据中的不同实体之间具有关系(例如包含用户、帖子和评论的博客)。

¥Many applications need to cache complex data in the store. That data is often received in a nested form from an API, or has relations between different entities in the data (such as a blog that contains Users, Posts, and Comments).

最好将该数据存储在存储的 "normalized" 表格 中。这使得根据 ID 查找商品和更新存储中的单个商品变得更加容易,并最终带来更好的性能模式。

¥Prefer storing that data in a "normalized" form in the store. This makes it easier to look up items based on their ID and update a single item in the store, and ultimately leads to better performance patterns.

保持状态最小并获得附加值

¥Keep State Minimal and Derive Additional Values

只要有可能,尽可能减少 Redux 存储中的实际数据,并根据需要从该状态派生其他值。这包括计算过滤列表或汇总值之类的事情。例如,待办事项应用会将待办事项对象的原始列表保留在状态中,但每当更新状态时都会在状态之外派生出已过滤的待办事项列表。类似地,也可以在存储外部计算检查所有待办事项是否已完成或剩余待办事项数量。

¥Whenever possible, keep the actual data in the Redux store as minimal as possible, and derive additional values from that state as needed. This includes things like calculating filtered lists or summing up values. As an example, a todo app would keep an original list of todo objects in state, but derive a filtered list of todos outside the state whenever the state is updated. Similarly, a check for whether all todos have been completed, or number of todos remaining, can be calculated outside the store as well.

这样做有几个好处:

¥This has several benefits:

  • 实际状态更容易阅读

    ¥The actual state is easier to read

  • 计算这些附加值并使它们与其余数据保持同步所需的逻辑更少

    ¥Less logic is needed to calculate those additional values and keep them in sync with the rest of the data

  • 原始状态仍然作为参考并且没有被替换

    ¥The original state is still there as a reference and isn't being replaced

导出数据通常在 "selector" 函数中完成,该函数可以封装进行导出数据计算的逻辑。为了提高性能,可以使用 reselectproxy-memoize 等库来记忆这些选择器以缓存以前的结果。

¥Deriving data is often done in "selector" functions, which can encapsulate the logic for doing the derived data calculations. In order to improve performance, these selectors can be memoized to cache previous results, using libraries like reselect and proxy-memoize.

将操作建模为事件,而不是设置者

¥Model Actions as Events, Not Setters

Redux 不关心 action.type 字段的内容是什么 - 它只需要被定义。以现在时 ("users/update")、过去时 ("users/updated")、描述为事件 ("upload/progress") 或视为 "setter" ("users/setUserName") 来编写动作类型是合法的。由你决定给定操作在你的应用中意味着什么,以及如何对这些操作进行建模。

¥Redux does not care what the contents of the action.type field are - it just has to be defined. It is legal to write action types in present tense ("users/update"), past tense ("users/updated"), described as an event ("upload/progress"), or treated as a "setter" ("users/setUserName"). It is up to you to determine what a given action means in your application, and how you model those actions.

但是,我们建议尝试将操作更多地视为 "描述发生的事件",而不是 "setters"。将操作视为 "events" 通常会导致更有意义的操作名称、更少的分派操作总数以及更有意义的操作日志历史记录。编写 "setters" 通常会导致过多的单独操作类型、过多的调度以及没有多大意义的操作日志。

¥However, we recommend trying to treat actions more as "describing events that occurred", rather than "setters". Treating actions as "events" generally leads to more meaningful action names, fewer total actions being dispatched, and a more meaningful action log history. Writing "setters" often results in too many individual action types, too many dispatches, and an action log that is less meaningful.

Detailed Explanation

想象一下,你有一个餐厅应用,有人点了一份披萨和一瓶可乐。你可以发送如下操作:

¥Imagine you've got a restaurant app, and someone orders a pizza and a bottle of Coke. You could dispatch an action like:

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

或者你可以发送:

¥Or you could dispatch:

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

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

第一个例子是 "event"。"嘿,有人点了一份披萨和一杯汽水,想办法处理一下吧"。

¥The first example would be an "event". "Hey, someone ordered a pizza and a pop, deal with it somehow".

第二个例子是 "setter"。"我知道有 '订购的披萨' 和 '流行音乐订购' 字段,我命令你将它们的当前值设置为这些数字"。

¥The second example is a "setter". "I know there are fields for 'pizzas ordered' and 'pops ordered', and I am commanding you to set their current values to these numbers".

"event" 方法实际上只需要调度一个操作,而且更加灵活。已经购买了多少披萨并不重要。也许没有厨师,所以订单会被忽略。

¥The "event" approach only really needed a single action to be dispatched, and it's more flexible. It doesn't matter how many pizzas were already ordered. Maybe there's no cooks available, so the order gets ignored.

使用 "setter" 方法,客户端代码需要更多地了解状态的实际结构是什么、"right" 值应该是什么,并且最终实际上必须分派多个操作来完成 "transaction"。

¥With the "setter" approach, the client code needed to know more about what the actual structure of the state is, what the "right" values should be, and ended up actually having to dispatch multiple actions to finish the "transaction".

写出有意义的动作名称

¥Write Meaningful Action Names

action.type 字段有两个主要用途:

¥The action.type field serves two main purposes:

  • reducer 逻辑检查操作类型以查看是否应处理此操作以计算新状态

    ¥Reducer logic checks the action type to see if this action should be handled to calculate a new state

  • Action 类型显示在 Redux DevTools 历史日志中供你阅读

    ¥Action types are shown in the Redux DevTools history log for you to read

根据 将动作建模为 "事件"type 字段的实际内容对 Redux 本身并不重要。然而,type 值对开发者来说确实很重要。操作应该使用有意义的、信息丰富的、描述性的类型字段来编写。理想情况下,你应该能够阅读已调度操作类型的列表,并充分了解应用中发生的情况,甚至无需查看每个操作的内容。避免使用非常通用的操作名称,例如 "SET_DATA""UPDATE_STORE",因为它们不能提供有关所发生事件的有意义的信息。

¥Per Model Actions as "Events", the actual contents of the type field do not matter to Redux itself. However, the type value does matter to you, the developer. Actions should be written with meaningful, informative, descriptive type fields. Ideally, you should be able to read through a list of dispatched action types, and have a good understanding of what happened in the application without even looking at the contents of each action. Avoid using very generic action names like "SET_DATA" or "UPDATE_STORE", as they don't provide meaningful information on what happened.

允许多个 reducer 响应相同的操作

¥Allow Many Reducers to Respond to the Same Action

Redux reducer 逻辑旨在分为许多较小的 reducer,每个 reducer 独立更新自己的状态树部分,然后全部组合在一起形成根 reducer 函数。当调度给定的操作时,它可能由所有、部分或不由任何 reducer 处理。

¥Redux reducer logic is intended to be split into many smaller reducers, each independently updating their own portion of the state tree, and all composed back together to form the root reducer function. When a given action is dispatched, it might be handled by all, some, or none of the reducers.

作为其中的一部分,如果可能的话,鼓励你让许多 reducer 函数都单独处理相同的操作。在实践中,经验表明大多数操作通常仅由单个 reducer 函数处理,这很好。但是,将操作建模为 "events" 并允许许多 reducer 响应这些操作通常将使你的应用的代码库能够更好地扩展,并最大限度地减少你需要分派多个操作来完成一个有意义的更新的次数。

¥As part of this, you are encouraged to have many reducer functions all handle the same action separately if possible. In practice, experience has shown that most actions are typically only handled by a single reducer function, which is fine. But, modeling actions as "events" and allowing many reducers to respond to those actions will typically allow your application's codebase to scale better, and minimize the number of times you need to dispatch multiple actions to accomplish one meaningful update.

避免按顺序分派许多操作

¥Avoid Dispatching Many Actions Sequentially

避免连续分派许多操作来完成更大的概念 "transaction"。这是合法的,但通常会导致多次相对昂贵的 UI 更新,并且某些中间状态可能会被应用逻辑的其他部分无效。更喜欢分派单个 "event" 类型的操作,该操作会立即导致所有适当的状态更新,或者考虑使用操作批处理插件来分派多个操作,最后仅进行单个 UI 更新。

¥Avoid dispatching many actions in a row to accomplish a larger conceptual "transaction". This is legal, but will usually result in multiple relatively expensive UI updates, and some of the intermediate states could be potentially invalid by other parts of the application logic. Prefer dispatching a single "event"-type action that results in all of the appropriate state updates at once, or consider use of action batching addons to dispatch multiple actions with only a single UI update at the end.

Detailed Explanation

你可以连续调度的操作数量没有限制。但是,每个分派的操作都会导致执行所有存储订阅回调(通常每个 Redux 连接的 UI 组件有一个或多个回调),并且通常会导致 UI 更新。

¥There is no limit on how many actions you can dispatch in a row. However, each dispatched action does result in execution of all store subscription callbacks (typically one or more per Redux-connected UI component), and will usually result in UI updates.

虽然从 React 事件处理程序排队的 UI 更新通常会批处理到单个 React 渲染通道中,但在这些事件处理程序之外排队的更新则不然。这包括大多数 async 函数的调度、超时回调和非 React 代码。在这些情况下,每次调度都会在调度完成之前产生完整的同步 React 渲染通道,这会降低性能。

¥While UI updates queued from React event handlers will usually be batched into a single React render pass, updates queued outside of those event handlers are not. This includes dispatches from most async functions, timeout callbacks, and non-React code. In those situations, each dispatch will result in a complete synchronous React render pass before the dispatch is done, which will decrease performance.

此外,概念上属于较大 "transaction" 式更新序列一部分的多个调度将导致可能被视为无效的中间状态。例如,如果连续调度操作 "UPDATE_A""UPDATE_B""UPDATE_C",并且某些代码期望 abc 的所有三个一起更新,则前两次调度之后的状态实际上将不完整,因为只有其中一两个已更新。

¥In addition, multiple dispatches that are conceptually part of a larger "transaction"-style update sequence will result in intermediate states that might not be considered valid. For example, if actions "UPDATE_A", "UPDATE_B", and "UPDATE_C" are dispatched in a row, and some code is expecting all three of a, b, and c to be updated together, the state after the first two dispatches will effectively be incomplete because only one or two of them has been updated.

如果确实需要多次调度,请考虑以某种方式批量更新。根据你的用例,这可能只是批处理 React 自己的渲染(可能使用 React-Redux 的 batch())、消除存储通知回调的抖动,或者将许多操作分组到一个更大的单个调度中,该调度仅产生一个订阅者通知。有关其他示例和相关插件的链接,请参阅 "减少存储更新事件" 上的常见问题解答条目

¥If multiple dispatches are truly necessary, consider batching the updates in some way. Depending on your use case, this may just be batching React's own renders (possibly using batch() from React-Redux), debouncing the store notification callbacks, or grouping many actions into a larger single dispatch that only results in one subscriber notification. See the FAQ entry on "reducing store update events" for additional examples and links to related addons.

评估每个状态应该在哪里

¥Evaluate Where Each Piece of State Should Live

"Redux 的三个原则" 表示 "整个应用的状态存储在单个树中"。这句话被过度解读了。这并不意味着整个应用中的每个值都必须保存在 Redux 存储中。相反,应该有一个地方可以找到你认为全局和应用范围内的所有值。"local" 的值通常应保留在最近的 UI 组件中。

¥The "Three Principles of Redux" says that "the state of your whole application is stored in a single tree". This phrasing has been over-interpreted. It does not mean that literally every value in the entire app must be kept in the Redux store. Instead, there should be a single place to find all values that you consider to be global and app-wide. Values that are "local" should generally be kept in the nearest UI component instead.

因此,作为开发者,你需要决定哪些状态应该实际存在于 Redux 存储中,哪些状态应该保留在组件状态中。使用这些经验法则来帮助评估每个状态并决定它应该位于哪里

¥Because of this, it is up to you as a developer to decide what state should actually live in the Redux store, and what should stay in component state. Use these rules of thumb to help evaluate each piece of state and decide where it should live.

使用 React-Redux Hooks API

¥Use the React-Redux Hooks API

更喜欢使用 React-Redux 钩子 API(useSelectoruseDispatch 作为从 React 组件与 Redux 存储交互的默认方式。虽然经典的 connect API 仍然可以正常工作并且将继续受到支持,但 hooks API 通常在几个方面更易于使用。与 connect 相比,这些钩子的间接性更少,需要编写的代码也更少,并且与 TypeScript 一起使用更简单。

¥Prefer using the React-Redux hooks API (useSelector and useDispatch) as the default way to interact with a Redux store from your React components. While the classic connect API still works fine and will continue to be supported, the hooks API is generally easier to use in several ways. The hooks have less indirection, less code to write, and are simpler to use with TypeScript than connect is.

hooks API 确实在性能和数据流方面引入了一些与 connect 不同的权衡,但我们现在建议将它们作为默认值。

¥The hooks API does introduce some different tradeoffs than connect does in terms of performance and data flow, but we now recommend them as the default.

Detailed Explanation

经典 connect API高阶分量。它生成一个新的封装器组件,该组件订阅存储,渲染你自己的组件,并将来自存储和操作创建者的数据作为属性传递。

¥The classic connect API is a Higher Order Component. It generates a new wrapper component that subscribes to the store, renders your own component, and passes down data from the store and action creators as props.

这是一个有意的间接级别,允许你编写 "presentational" 风格的组件,这些组件接收所有值作为 props,而无需专门依赖 Redux。

¥This is a deliberate level of indirection, and allows you to write "presentational"-style components that receive all their values as props, without being specifically dependent on Redux.

钩子的引入改变了大多数 React 开发者编写组件的方式。虽然 "容器/展示" 概念仍然有效,但钩子会促使你编写负责通过调用适当的钩子在内部请求自己的数据的组件。这导致我们编写和测试组件和逻辑的方法不同。

¥The introduction of hooks has changed how most React developers write their components. While the "container/presentational" concept is still valid, hooks push you to write components that are responsible for requesting their own data internally by calling an appropriate hook. This leads to different approaches in how we write and test components and logic.

connect 的间接性总是让一些用户很难理解数据流。此外,由于多重重载、可选参数、来自 mapState / mapDispatch / 父组件的 props 合并以及操作创建者和 thunk 的绑定,connect 的复杂性使得使用 TypeScript 正确输入变得非常困难。

¥The indirection of connect has always made it a bit difficult for some users to follow the data flow. In addition, connect's complexity has made it very difficult to type correctly with TypeScript, due to the multiple overloads, optional parameters, merging of props from mapState / mapDispatch / parent component, and binding of action creators and thunks.

useSelectoruseDispatch 消除了间接性,因此你自己的组件如何与 Redux 交互更加清晰。由于 useSelector 仅接受单个选择器,因此使用 TypeScript 定义要容易得多,useDispatch 也是如此。

¥useSelector and useDispatch eliminate the indirection, so it's much more clear how your own component is interacting with Redux. Since useSelector just accepts a single selector, it's much easier to define with TypeScript, and the same goes for useDispatch.

有关更多详细信息,请参阅 Redux 维护者 Mark Erikson 关于 Hooks 和 HOC 之间权衡的帖子和会议演讲:

¥For more details, see Redux maintainer Mark Erikson's post and conference talk on the tradeoffs between hooks and HOCs:

另请参阅 React-Redux 钩子 API 文档,了解有关如何正确优化组件和处理罕见边缘情况的信息。

¥Also see the React-Redux hooks API docs for info on how to correctly optimize components and handle rare edge cases.

连接更多组件以从存储中读取数据

¥Connect More Components to Read Data from the Store

更喜欢让更多 UI 组件订阅 Redux 存储并以更细粒度的级别读取数据。这通常会带来更好的 UI 性能,因为当给定的状态发生变化时需要渲染的组件更少。

¥Prefer having more UI components subscribed to the Redux store and reading data at a more granular level. This typically leads to better UI performance, as fewer components will need to render when a given piece of state changes.

例如,不只是连接 <UserList> 组件并读取整个用户数组,而是让 <UserList> 检索所有用户 ID 的列表,将列表项渲染为 <UserListItem userId={userId}>,并连接 <UserListItem> 并从存储中提取自己的用户条目。

¥For example, rather than just connecting a <UserList> component and reading the entire array of users, have <UserList> retrieve a list of all user IDs, render list items as <UserListItem userId={userId}>, and have <UserListItem> be connected and extract its own user entry from the store.

这适用于 React-Redux connect() API 和 useSelector() 钩子。

¥This applies for both the React-Redux connect() API and the useSelector() hook.

使用 mapDispatchconnect 的对象简写形式

¥Use the Object Shorthand Form of mapDispatch with connect

connectmapDispatch 参数可以定义为接收 dispatch 作为参数的函数,也可以定义为包含操作创建者的对象。我们建议始终使用 mapDispatch 的 "对象简写" 形式,因为它可以大大简化代码。几乎从来没有真正需要将 mapDispatch 编写为函数。

¥The mapDispatch argument to connect can be defined as either a function that receives dispatch as an argument, or an object containing action creators. We recommend always using the "object shorthand" form of mapDispatch, as it simplifies the code considerably. There is almost never a real need to write mapDispatch as a function.

在函数组件中多次调用 useSelector

¥Call useSelector Multiple Times in Function Components

使用 useSelector 钩子检索数据时,最好多次调用 useSelector 并检索较少量的数据,而不是在对象中返回多个结果的单个较大的 useSelector 调用。与 mapState 不同,useSelector 不需要返回对象,并且选择器读取较小的值意味着给定的状态更改不太可能导致该组件渲染。

¥When retrieving data using the useSelector hook, prefer calling useSelector many times and retrieving smaller amounts of data, instead of having a single larger useSelector call that returns multiple results in an object. Unlike mapState, useSelector is not required to return an object, and having selectors read smaller values means it is less likely that a given state change will cause this component to render.

但是,请尝试找到适当的粒度平衡。如果单个组件确实需要 state 切片中的所有字段,只需编写一个返回整个切片的 useSelector,而不是为每个单独的字段使用单独的选择器。

¥However, try to find an appropriate balance of granularity. If a single component does need all fields in a slice of the state , just write one useSelector that returns that whole slice instead of separate selectors for each individual field.

使用静态类型

¥Use Static Typing

使用 TypeScript 或 Flow 等静态类型系统,而不是纯 JavaScript。类型系统将捕获许多常见错误,改进代码文档,并最终带来更好的长期可维护性。虽然 Redux 和 React-Redux 最初设计时考虑的是普通 JS,但两者都可以很好地与 TS 和 Flow 配合使用。Redux Toolkit 是专门用 TS 编写的,旨在通过最少的附加类型声明提供良好的类型安全性。

¥Use a static type system like TypeScript or Flow rather than plain JavaScript. The type systems will catch many common mistakes, improve the documentation of your code, and ultimately lead to better long-term maintainability. While Redux and React-Redux were originally designed with plain JS in mind, both work well with TS and Flow. Redux Toolkit is specifically written in TS and is designed to provide good type safety with a minimal amount of additional type declarations.

使用 Redux DevTools 扩展进行调试

¥Use the Redux DevTools Extension for Debugging

配置你的 Redux 存储以启用 使用 Redux DevTools 扩展进行调试。它允许你查看:

¥Configure your Redux store to enable debugging with the Redux DevTools Extension. It allows you to view:

此外,DevTools 允许你执行 "时间旅行调试",在操作历史记录中来回查看不同时间点的整个应用状态和 UI。

¥In addition, the DevTools allows you to do "time-travel debugging", stepping back and forth in the action history to see the entire app state and UI at different points in time.

Redux 是专门为实现这种调试而设计的,DevTools 是使用 Redux 的最有力的原因之一。

¥Redux was specifically designed to enable this kind of debugging, and the DevTools are one of the most powerful reasons to use Redux.

使用纯 JavaScript 对象作为状态

¥Use Plain JavaScript Objects for State

更喜欢在状态树中使用纯 JavaScript 对象和数组,而不是像 Immutable.js 这样的专用库。虽然使用 Immutable.js 有一些潜在的好处,但大多数常见目标(例如简单的引用比较)通常都是不可变更新的属性,并且不需要特定的库。这也使得包的大小更小,并降低了数据类型转换的复杂性。

¥Prefer using plain JavaScript objects and arrays for your state tree, rather than specialized libraries like Immutable.js. While there are some potential benefits to using Immutable.js, most of the commonly stated goals such as easy reference comparisons are a property of immutable updates in general, and do not require a specific library. This also keeps bundle sizes smaller and reduces complexity from data type conversions.

如上所述,如果你想简化不可变的更新逻辑,特别是作为 Redux Toolkit 的一部分,我们特别建议使用 Immer。

¥As mentioned above, we specifically recommend using Immer if you want to simplify immutable update logic, specifically as part of Redux Toolkit.

Detailed Explanation

Immutable.js 从一开始就在 Redux 应用中频繁使用。使用 Immutable.js 有几个常见原因:

¥Immutable.js has been semi-frequently used in Redux apps since the beginning. There are several common reasons stated for using Immutable.js:

  • 通过廉价的参考比较提高性能

    ¥Performance improvements from cheap reference comparisons

  • 通过专门的数据结构进行更新可以提高性能

    ¥Performance improvements from making updates thanks to specialized data structures

  • 预防意外突变

    ¥Prevention of accidental mutations

  • 通过 setIn() 等 API 更轻松地进行嵌套更新

    ¥Easier nested updates via APIs like setIn()

这些原因有一些合理的方面,但在实践中,其好处并不像所说的那么好,而且使用它有多种负面影响:

¥There are some valid aspects to those reasons, but in practice, the benefits aren't as good as stated, and there's multiple negatives to using it:

  • 便宜的参考比较是任何不可变更新的属性,而不仅仅是 Immutable.js

    ¥Cheap reference comparisons are a property of any immutable updates, not just Immutable.js

  • 可以通过其他机制来防止意外突变,例如使用 Immer(消除了容易发生意外的手动复制逻辑,并默认深度冻结开发中的状态)或 redux-immutable-state-invariant(检查突变状态)

    ¥Accidental mutations can be prevented via other mechanisms, such as using Immer (which eliminates accident-prone manual copying logic, and deep-freezes state in development by default) or redux-immutable-state-invariant (which checks state for mutations)

  • Immer 总体上允许更简单的更新逻辑,无需 setIn()

    ¥Immer allows simpler update logic overall, eliminating the need for setIn()

  • Immutable.js 的包大小非常大

    ¥Immutable.js has a very large bundle size

  • API 相当复杂

    ¥The API is fairly complex

  • API "infects" 你的应用代码。所有逻辑都必须知道它是在处理普通 JS 对象还是 Immutable 对象

    ¥The API "infects" your application's code. All logic must know whether it's dealing with plain JS objects or Immutable objects

  • 从 Immutable 对象转换为普通 JS 对象的成本相对较高,并且总是会产生全新的深层对象引用

    ¥Converting from Immutable objects to plain JS objects is relatively expensive, and always produces completely new deep object references

  • 库缺乏持续维护

    ¥Lack of ongoing maintenance to the library

使用 Immutable.js 的最有力的理由是快速更新非常大的对象(数万个键)。大多数应用不会处理那么大的对象。

¥The strongest remaining reason to use Immutable.js is fast updates of very large objects (tens of thousands of keys). Most applications won't deal with objects that large.

总的来说,Immutable.js 增加了太多的开销,但实际收益却太少。Immer 是一个更好的选择。

¥Overall, Immutable.js adds too much overhead for too little practical benefit. Immer is a much better option.