操作(Actions)
Redux 常见问题:操作(Actions)
为什么 type 应该是字符串?为什么 action 类型应该是常量?
与状态一样,可序列化的操作支持 Redux 的几个核心特性,比如时间旅行调试,以及记录和重放操作。如果 type 使用类似 Symbol 的类型或对操作本身使用 instanceof 检查,就会破坏这些功能。字符串是可序列化且自描述的,因此是更好的选择。请注意,如果操作旨在由中间件使用,操作中使用 Symbols、Promises 或其他不可序列化的值也是可以的。操作只需在真正到达 store 并传递给 reducer 时是可序列化的。
出于性能考虑,我们无法可靠地强制所有操作都可序列化,所以 Redux 仅检查每个操作必须是一个普通对象,且 type 是字符串。其余由你自己决定,但你可能会发现保持一切可序列化有助于调试和重现问题。
封装并集中常用代码片段是编程的关键概念。虽然确实可以在任何地方手动创建操作对象并手写每个 type 字符串,但定义可复用的常量会使代码维护更便捷。如果把常量放到单独的文件中,还可以通过工具检查导入语句中的拼写错误,防止意外使用错误的字符串。
如今通常不需要手动编写 action 类型常量。 createSlice 会根据 slice 的 name 和 reducers 字段生成类型字符串(格式为 sliceName/reducerName,例如 'todos/todoAdded')及对应的 action creator,并将 reducer 与这些类型关联起来。派发生成的 action creator,让 TypeScript 帮你捕获拼写错误。
更多信息
文档
讨论
- #384: 建议将操作常量命名为过去式
- #628: 简化操作创建的方案
- #1024: 提案:声明式 reducer
- #1167: 不用 switch 的 reducer
- Stack Overflow: 为什么 Redux 需要“操作”作为数据?
- Stack Overflow: Redux 中定义常量的意义是什么?
reducer 和操作之间是否总是一一对应的?
不是。我们建议编写独立的小型 reducer 函数,每个 reducer 负责特定状态片段的更新。我们称这种模式为“reducer 组合”。一个操作可以被全部、部分或无任何 reducer 处理。这样保持组件与实际数据变动解耦,因为一个操作可能影响状态树的不同部分,组件无需知道这些细节。一些用户会选择更紧密地绑定它们,比如“ducks” 文件结构,但默认情况下绝不存在一一对应关系。如果你发现需要在多个 reducer 中处理同一个操作,应该跳出一一对应的思维模式。
更多信息
文档
讨论
- Twitter:最常见的 Redux 误解
- #1167: 不用 switch 的 reducer
- Reduxible #8: Reducers 和 action creators 不是一一对应
- Stack Overflow: 能否在没有 Redux Thunk 中间件的情况下分发多个操作?
如何表现 “副作用” 例如 AJAX 请求?为什么需要 “操作创建者”、“thunks” 和 “中间件” 来处理异步行为?
这是一个复杂且意见多样的话题,关于代码应如何组织以及应采用何种方案各家观点不一。
任何有意义的 Web 应用都需要执行复杂逻辑,通常包括异步工作,如 AJAX 请求。这类代码不再单纯是输入的函数,其与外部世界的交互称为“副作用”。
Redux 受到函数式编程的启发,开箱即用时没有执行副作用的方案。具体来说,reducer 函数必须始终是纯函数,形式为 (state, action) => newState。但 Redux 的中间件可以拦截派发的操作并在周围添加复杂行为,包括副作用。
通常,Redux 建议把副作用代码放进操作创建流程中。尽管这些逻辑可以写在 UI 组件中,但通常更合理抽象成可复用函数,这样同一逻辑可被多处调用,也就是操作创建者函数。
最简单、最常见的方式是使用 Redux Thunk 中间件,以便在 action creator 中编写更复杂的异步逻辑。configureStore 默认会添加它,而 createAsyncThunk 会为基于 Promise 的 thunk 生成 pending、fulfilled 和 rejected action。对于数据获取和缓存,RTK Query 会处理完整的请求生命周期,你无需自行编写这些逻辑。listener middleware 适用于“某个 action 派发后运行副作用”的场景。此外,Redux Saga 使用 generator 编写更接近同步形式的代码,也能在 Redux 应用中充当“后台线程”;社区中还有其他用于管理副作用的库。
更多信息
文档
文章
讨论
- #291: 试图把 API 调用放到合适位置
- #455: 副作用建模
- #533: 简明介绍异步操作创建者
- #569: 提案:显式副作用 API
- #1139: 基于 generator 和 saga 的替代副作用模型
- Stack Overflow: 为什么 Redux 中异步流程需要中间件?
- Stack Overflow: 如何带超时分发 Redux 操作?
- Stack Overflow: Redux 中同步副作用该放哪里?
- Stack Overflow: 如何在 Redux 中处理复杂副作用?
- Stack Overflow: 如何单元测试异步 Redux 操作并模拟 ajax 响应?
- Stack Overflow: 如何在 Redux 状态变化时触发 AJAX 请求?
- Reddit: 使用 Redux-Promise Middleware 完成异步 API 调用的帮助
- Twitter: 可能的 saga、loops 和其他方案对比
应该使用哪种异步中间件?如何在 thunks、sagas、observables 或其他方案间做选择?
目前有许多异步或副作用中间件可选,最常用的包括 redux-thunk、Redux Toolkit 的 listener middleware、redux-saga 和 redux-observable。它们各有优缺点,适用场景也不同。
通用建议:
- 如果逻辑用于从服务器获取并缓存数据,请使用 RTK Query,无需自行实现这部分逻辑。
- Thunk 适用于复杂的同步逻辑(尤其是需要访问整个 Redux store 状态的代码)以及简单的异步逻辑(例如基本的 AJAX 请求)。使用
async/await后,thunk 也可以处理一些更复杂的 Promise 逻辑。 - Listener middleware 适用于_响应_已派发 action 或状态变化的逻辑,例如“用户登录后开始轮询”或“添加待办后持久化列表”。它也可以取消或防抖任务。无需使用 generator 函数,就能覆盖 saga 的许多常见用途。
- Saga 适用于复杂异步工作流,以及 listener middleware 无法处理的、彼此解耦的“后台线程”式行为。使用它需要熟悉 generator 函数和
redux-saga的“effects”操作符。 - Observable 解决的问题与 saga 类似,但依赖 RxJS 实现异步行为,因此需要熟悉 RxJS API。
我们建议大多数 Redux 用户使用 RTK Query 获取数据,使用 thunk 处理其他逻辑;需要响应 action 时再加入 listener middleware,只有应用确实需要处理更复杂的异步工作流时,才考虑 saga 或 observable。更详细的比较请参阅风格指南中的“使用 thunk 和 listener 处理其他异步逻辑”以及副作用处理方案。
因为 sagas 和 observables 适用场景相同,一个应用一般选其一,而不会两者兼用。但注意,使用 thunks 以及 sagas 或 observables 其一是完全可以共存的,因为它们解决的是不同的问题。
文章
讨论
- Reddit: 关于同时使用 thunks 和 sagas 以及 sagas 优缺点的讨论
- Stack Overflow: 使用 ES2015 生成器的 redux-saga 与 ES2017 async/await 的 redux-thunk 优缺点
- Stack Overflow: 为什么使用 Redux-Observable 而非 Redux-Saga?
我应该在一个操作创建者中连续派发多个操作吗?
对于操作的结构没有硬性规定。像 Redux Thunk 这样的异步中间件当然支持连续派发多个相关但不同的操作,派发表示 AJAX 请求进度的操作,根据状态条件派发操作,甚至派发某个操作后立即检查更新的状态。
一般来说,考虑这些操作是相关但独立的,还是应该合并为一个操作。根据你的具体情况做决定,但要权衡 reducer 的可读性和操作日志的可读性。例如,一个包含整个新状态树的操作会让 reducer 变成一行代码,但缺点是你失去了变化原因的历史记录,调试会非常困难。反之,如果你通过循环发送多个细粒度操作,那可能意味着你应该引入一种新的操作类型,并通过不同方式处理。
如果你关心性能,尽量避免在同一时刻连续同步派发多次操作。有许多插件和方案可以对 dispatch 操作进行合并批处理。
更多信息
文档
文章
讨论