Redux Style Guide:Redux 应用的官方编码规范与最佳实践实战手册
本文系统讲解 Redux 官方风格指南(Style Guide)中定义的全部规则体系:A 级(必须遵守、防止 bug)、B 级(强烈建议、提升可读性与可维护性)、C 级(推荐选择、保证团队一致性)。指南中的每条规则都会结合当前仓库中的核心源码(createStore、combineReducers)与官方示例工程(counter、todomvc、real-world)做印证,读完后你不仅能知道"Redux 应该怎么写",还能理解每条规则背后 Redux 运行机制层面的依据。
规则分类:三个优先级,不同违反成本
官方风格指南(docs/style-guide/style-guide.md)开篇就明确了定位:Redux 核心库和文档本身是"不持立场"的,使用方式有很多种,很多时候不存在唯一的"正确"写法。但经验表明,某些主题上某些做法明显更优,且大量开发者希望有官方指引来减少决策疲劳。因此该指南列出了一份推荐清单,帮助开发者避开错误、无谓争论(bikeshedding)和反模式,同时强调:团队偏好各异,指南并非放之四海皆准,应结合自身项目评估后再决定是否采纳。
规则被分为三个优先级:
| 优先级 | 名称 | 违反的后果与约束程度 |
|---|---|---|
| Priority A | Essential(必须) | 帮助防止错误,应不惜代价学习并遵守。例外应极少出现,且只有同时精通 JavaScript 与 Redux 的专家才有权做出例外 |
| Priority B | Strongly Recommended(强烈建议) | 在大多数项目中被证明能提升可读性和/或开发体验。违反时代码依然能运行,但违反应是罕见且有充分理由的 |
| Priority C | Recommended(推荐) | 存在多个同等好的选项时,任选其一即可保证一致性。指南会描述每个可接受选项并建议一个默认选择;你可以做不同选择,只要全代码库一致且有充分理由 |
下面逐条展开,并在每条规则后给出本仓库源码或示例中的对应证据。
Priority A 规则:必须遵守(防错误)
1. 不要修改(Mutate)State
修改 state 是 Redux 应用中最常见的 bug 来源,会导致组件无法正常重新渲染,同时会破坏 Redux DevTools 的时间旅行(time-travel)调试。无论是 reducer 内部还是其他任何应用代码,都应始终避免对 state 值的实际修改。指南推荐:
- 开发阶段用
redux-immutable-state-invariant之类的工具捕获 mutation; - 用 Immer 避免 state 更新中的意外修改。
注意(原文档明确说明的边界):修改现有值的副本是允许的——这是编写不可变更新逻辑的正常部分。使用 Immer 时写"mutating 风格"逻辑也是可接受的,因为真实数据并未被修改,Immer 会安全地追踪变更并内部生成不可变更新后的新值。
仓库示例直接印证了这一点。counter 示例的 counterSlice.js 中:
reducers: {
increment: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
...
}
注释明确说明:这里看起来像修改,实际上 Redux Toolkit 底层用 Immer 检测对"draft state"的变更并生成全新的不可变 state。
2. Reducer 不允许有副作用
Reducer 函数应当只依赖 state 和 action 两个参数,只基于这两个参数计算并返回新的 state 值。它不得执行任何异步逻辑(AJAX 请求、timeout、Promise)、生成随机值(Date.now()、Math.random())、修改变量、或运行任何影响 reducer 作用域之外事物的代码。例外说明:reducer 调用外部定义的其他函数(如从库或工具函数导入)是允许的,前提是这些函数遵循同样的规则。
这条规则的目的是保证 reducer 被调用时行为可预测。比如做时间旅行调试时,reducer 函数可能会被用更早的 action 反复调用多次以重建"当前"状态;如果 reducer 有副作用,这些副作用就会在调试过程中被重复执行,导致应用表现意外。原文档也坦诚指出灰色地带:严格说 console.log(state) 是副作用,但实践中它不影响应用行为。
这条规则在源码层面有直接支撑。createStore 的 dispatch() 实现中有一个 isDispatching 标志位,reducer 执行期间若尝试再 dispatch 会直接抛错 "Reducers may not dispatch actions."(见 src/createStore.ts)。整个 state 的计算就发生在:
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
reducer 被当作纯函数调用:输入当前 state 和 action,输出必须是新 state,且过程中不能反查 store(getState() 在 dispatch 期间同样被禁止,见 src/createStore.ts)。这正是"reducer 必须是纯函数"在实现层的强制体现。
3. 不要把不可序列化的值放进 State 或 Actions
避免将 Promise、Symbol、Map/Set、函数、类实例等不可序列化的值放进 Redux store 的 state 或已 dispatch 的 action 中。这样做能确保 Redux DevTools 调试等功能按预期工作,也确保 UI 按预期更新。
例外:如果 action 会在到达 reducer 之前被中间件拦截并"截停",那么可以在 action 中放不可序列化值。redux-thunk、redux-promise 就是这类中间件的例子。
这个规则的边界在 dispatch() 的源码校验中同样清晰:
if (!isPlainObject(action)) {
throw new Error(
`Actions must be plain objects. ... You may need to add middleware
to your store setup to handle dispatching other values, such as
'redux-thunk' to handle dispatching functions. ...`
)
}
(见 src/createStore.ts)——基础 dispatch 只接受 plain object 且 type 必须是字符串,想 dispatch 函数/Promise 必须靠中间件。这与"默认拒绝、中间件豁免"的指南表述完全一致。
4. 每个 App 只有一个 Redux Store
标准 Redux 应用应该只创建一个 Redux store 实例,供整个应用使用,通常定义在 store.js 之类的独立文件中。理想情况下,应用逻辑不应直接 import store:它应该通过 <Provider> 传入 React 组件树,或经由 thunk 等中间件间接引用。只有在极少数情况下才需要把 store 直接导入其他逻辑文件,且这应是最后手段。
这条规则在 Redux 核心源码的 API 文档注释里被原样重申:"There should only be a single store in your app"(见 src/createStore.ts)。counter 示例展示了标准做法——store 定义在独立文件 examples/counter/src/app/store.js:
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
export const store = configureStore({
reducer: {
counter: counterReducer
}
})
Priority B 规则:强烈建议
5. 用 Redux Toolkit 编写 Redux 逻辑
Redux Toolkit(RTK)是官方推荐的使用 Redux 的工具集。它内置了指南建议的最佳实践:configureStore 会在开发环境自动捕获 mutation、启用 Redux DevTools 扩展集成,配合 Immer 简化不可变更新逻辑等。指南同时强调:你并不被强制要求使用 RTK,可以自由选择其他方案,但使用 RTK 会简化逻辑并保证应用以良好的默认配置起步。
这一点已经体现在仓库核心源码的"弃用警告"注释中:createStore 的文档注释直接标注 @deprecated,推荐"使用 @reduxjs/toolkit 的 configureStore 方法……你如今不应把 redux 核心包单独用于生产应用,仅可用于学习目的;如需绕过弃用提示,可改用 legacy_createStore 导入"(见 src/createStore.ts)。所有示例工程的 store 也统一采用 configureStore,如 examples/counter/src/app/store.js 与 examples/counter-ts/src/app/store.ts。
6. 用 Immer 编写不可变更新
手写的不可变更新逻辑经常困难且易错。Immer 允许用"mutative 风格"的简单逻辑编写不可变更新,并且可以在开发环境冻结(freeze)state 以捕获应用中其他地方的意外修改。指南建议用 Immer 编写不可变更新逻辑,最好作为 Redux Toolkit 的一部分使用——前文 counterSlice 的 state.value += 1 写法即是该规则的落地形态(见 examples/counter-ts/src/features/counter/counterSlice.ts)。
7. 以"功能文件夹 + 单文件逻辑"组织文件结构
Redux 本身不关心你的文件夹与文件如何组织,但把某个功能的逻辑集中在同一处通常更易维护。因此指南推荐大多数应用采用"feature folder"方式组织文件(一个功能的所有文件放在同一个文件夹);在每个功能文件夹内,该功能的 Redux 逻辑应写成一个"slice"文件,最好使用 RTK 的 createSlice API(这就是俗称的"ducks"模式)。旧代码库常用的"按类型分文件夹"(actions 一个文件夹、reducers 一个文件夹)把相关逻辑拆散了,集中在一起更容易查找和更新。
原文档给出的典型目录结构如下,可对照仓库示例直接验证——examples/counter/src 就是该结构的实例:
/srcindex.tsx:渲染 React 组件树的入口文件/appstore.ts:store 配置rootReducer.ts:根 reducer(可选)App.tsx:根 React 组件
/common:通用 hooks、组件、工具函数等/features:包含所有"功能文件夹"/todos:单个功能文件夹todosSlice.ts:Redux reducer 逻辑及关联 actionTodos.tsx:React 组件
其中 /app 放依赖所有其他文件夹的应用级配置与布局;/common 放真正通用可复用的工具与组件;/features 下每个子文件夹包含与某一功能相关的全部代码,todosSlice.ts 即为"duck"式单文件,内含 createSlice() 调用并导出 slice reducer 与 action creators。仓库中的 examples/counter/src/features/counter 目录(counterSlice.js、counterAPI.js、Counter.js、Counter.module.css 同居一处)正是这一结构的真实写照。
8. 尽可能把逻辑放进 Reducer
尽量把计算新状态的逻辑放进相应的 reducer,而不是放在准备和 dispatch action 的代码中(比如点击处理器)。这能让更多真实应用逻辑易于测试、更有效地利用时间旅行调试、并规避导致修改与 bug 的常见错误。当然也存在合法场景需要先算出新状态(如生成唯一 ID),但应最小化。
原文档用 todo 应用的"toggle todo"完整对比了两种写法。方式一,action 只携带 ID,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 };
})
}
方式二,先把新数组算好整个放进 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;
Redux 核心并不关心新 state 在哪里计算,但把逻辑放在 reducer 里更好,原因有五:
- reducer 总是易于测试——纯函数,只需
const result = reducer(testState, action)然后断言结果。逻辑越多,可测代码越多; - state 更新必须遵守不可变更新规则(参见 docs/usage/structuring-reducers/ImmutableUpdatePatterns.md)。多数用户知道在 reducer 内必须遵守,但没意识到在 reducer 外计算新 state 时同样必须遵守——在外部计算容易引发意外修改、甚至从 store 读值后原样塞回 action;
- 使用 RTK/Immer 时,在 reducer 中写不可变更新更容易,Immer 还会冻结 state 捕获意外修改;
- 时间旅行调试依赖"撤销 action → 改变/重做",reducer 热更新通常是用已有 action 重跑新 reducer。如果 action 正确而 reducer 有 bug,修好 reducer 热重载即可立即得到正确 state;若 action 本身错了,就必须重新走一遍产生该 action 的操作序列;
- 逻辑集中在 reducer 中,你知道去哪里找更新逻辑,而不是散落在应用代码各处。
9. Reducer 应掌控 State 的形状
Redux 根 state 由唯一的根 reducer 函数拥有并计算。为可维护性,该 reducer 应按 key/value "切片"拆分,每个"slice reducer"负责提供初始值并计算该切片的更新。此外,slice reducer 应掌控返回状态中包含哪些值:最小化"盲拷贝/盲返回"(blind spreads/returns),如 return action.payload 或 return {...state, ...action.payload},因为它们依赖 dispatch 方正确格式化内容,reducer 实际上放弃了对 state 形状的掌控权,一旦 action 内容不正确就会引发 bug。
注意(原文档给出的豁免):像表单编辑数据这类场景,为每个字段写独立 action type 费时且收益低,"spread 返回"的 reducer 可能是合理选择。
原文档用一个"当前用户"reducer 演示风险:
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 是格式正确的对象。若某处代码错误地 dispatch 了一个 todo 对象:
dispatch({
type: 'users/userLoggedIn',
payload: { id: 42, text: 'Buy milk' }
})
reducer 会盲目返回这个 todo,随后应用其他部分读取用户时大概率崩溃。缓解办法是让 reducer 做校验、或按字段名逐一读取——代价是更多代码,需在代码量与安全性之间权衡。使用静态类型会让此类代码更安全:如果 reducer 知道 action 是 PayloadAction<User>,那么 return action.payload 在类型层面就是安全的。
这一条与 combineReducers 的实现相互呼应:它会先探测每个 slice reducer(用 undefined state 和一个随机 action 各调用一次)并断言其永远不能返回 undefined,从而保证"每个 slice 必须自己提供初始值"——见 src/combineReducers.ts 中的 assertReducerShape。
10. 按存储的数据命名 State 切片
根 state 按"切片"拆分,combineReducers 是把这些 slice reducer 组合成更大 reducer 的标准函数。传给 combineReducers 的对象的 key 名会直接决定结果 state 对象的 key 名,因此 key 应以后面存储的数据命名,避免在 key 名中出现"reducer"字样。对象应该像 {users: {}, posts: {}},而不是 {usersReducer: {}, postsReducer: {}}。
原文档指出这是一个由对象字面量简写引发的常见错误:
import usersReducer from 'features/users/usersSlice'
const rootReducer = combineReducers({
usersReducer // 简写后变成 {usersReducer: usersReducer}
})
"reducer" 因此混进了 state 的 key 名,冗余且无意义。正确做法是用显式 key: value 语法:
import usersReducer from 'features/users/usersSlice'
import postsReducer from 'features/posts/postsSlice'
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer
})
多敲几个字符,换来最清晰易懂的代码与 state 定义。仓库示例是这一规则的正例:examples/todomvc/src/reducers/index.js 用数据名(而非 reducer 变量名)作为 key:
const rootReducer = combineReducers({
todos,
visibilityFilter
})
11. 按数据类型组织 State 结构,而非按组件
根 state 切片应基于应用的主要数据类型或功能域来定义和命名,而不是基于 UI 中的具体组件。Redux store 中的数据与 UI 组件之间并不存在严格 1:1 对应关系,很多组件可能需要同一段数据。可以把 state 树视为一种全局数据库,应用任何部分都可以访问它,读取该组件需要的部分 state。例如一个博客应用需要跟踪登录用户、作者与文章信息、当前屏幕等,好的结构是 {auth, posts, users, ui},坏的结构是 {loginScreen, usersList, postsList}。
12. 把 Reducer 当作状态机
许多 Redux reducer 是"无条件"书写的:只看 dispatch 的 action 就计算新 state,完全不管当前 state。这会引发 bug,因为某些 action 在特定时机概念上并不"合法"——例如"请求成功"action 只有当 state 显示正在 loading 时才应计算新值;"更新该项"action 只有当确有项被标记为"编辑中"时才应生效。修复方式:把 reducer 当作"状态机",由当前 state 与 dispatch 的 action 的组合共同决定是否计算新 state,而不是只看 action 本身。
原文档用有限状态机(FSM)建模 fetchUserReducer,四个有限状态:"idle"(未开始)、"loading"(获取中)、"success"(成功)、"failure"(失败)。通过显式 status 字段让状态可见、让不可能的状态不可能:
const initialUserState = {
status: 'idle', // explicit finite state
user: null,
error: null
}
用 TypeScript 时还可以用可辨识联合(discriminated unions)表达:若 state.status === 'success',则期望 state.user 有定义且 state.error 为假,可用类型强制。写状态机式 reducer 的关键是先考虑 state,再考虑 action,为每个状态创建"finite state reducer"封装该状态下的行为:
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;
}
}
按状态而非按 action 定义行为,还能防止不可能的状态转移:比如 status === LOADING_STATUS 时 FETCH_USER 不应生效,你可以强制这一点,而不是意外引入边界问题。
仓库示例展示了该模式在 createSlice 中的简化形态:examples/counter-ts/src/features/counter/counterSlice.ts 的 state 类型声明为 { value: number; status: 'idle' | 'loading' | 'failed' },extraReducers 中按 pending/fulfilled/rejected 分别把 status 在有限状态之间转移——这正是"state + action 共同决定新 state"的 RTK 实现。
13. 规范化(Normalize)复杂的嵌套/关系型 State
很多应用需要在 store 中缓存复杂数据,这类数据常以嵌套形式从 API 返回,或实体间存在关系(如博客包含 Users、Posts、Comments)。建议以"规范化"形式存储(参见 docs/usage/structuring-reducers/NormalizingStateShape.md)。这样按 ID 查找条目、更新 store 中的单个条目都更容易,最终导向更好的性能模式。
14. 保持 State 最小化,派生额外值
尽可能让 Redux store 中的实际数据保持最小,需要时从 state 派生(derive)额外值,包括计算过滤后的列表、求和等。例如 todo 应用应在 state 中保留原始 todo 对象列表,过滤后的列表在 state 更新时在 state 之外派生;"是否全部完成""剩余多少个"也都在 store 外计算。好处有三:state 更易读;计算这些值并与其余数据保持同步所需逻辑更少;原始 state 仍在作为参照,不会被替换。
派生数据常在"selector 函数"中完成,可用 reselect、proxy-memoize 等库对 selector 做记忆化(memoize)以缓存结果、提升性能。仓库示例正是"state 存原始数据、selector 做派生"的教科书案例,examples/todomvc/src/selectors/index.js:
import { createSelector } from 'reselect'
const getVisibilityFilter = state => state.visibilityFilter
const getTodos = state => state.todos
// 派生值:按过滤器过滤后的可见 todo,而非在 state 里存一份过滤后的副本
export const getVisibleTodos = createSelector(
[getVisibilityFilter, getTodos],
(visibilityFilter, todos) => {
switch (visibilityFilter) {
case SHOW_ALL: return todos
case SHOW_COMPLETED: return todos.filter(t => t.completed)
case SHOW_ACTIVE: return todos.filter(t => !t.completed)
default: throw new Error('Unknown filter: ' + visibilityFilter)
}
}
)
// 派生值:已完成计数
export const getCompletedTodoCount = createSelector([getTodos], todos =>
todos.reduce((count, todo) => (todo.completed ? count + 1 : count), 0)
)
15. 把 Action 建模为"事件",而非"Setter"
Redux 不关心 action.type 字段的内容是什么——它只要被定义即可。以现在时("users/update")、过去时("users/updated")、事件描述("upload/progress")或"setter"("users/setUserName")命名 action type 都是合法的,由你决定 action 在应用中的含义。但指南建议尽量把 action 当作"描述已发生的事件",而非"setter"。事件建模通常带来更有意义的 action 名、更少的 dispatch 次数、以及更有意义的 action 日志历史;写成 setter 往往导致 action type 过多、dispatch 过多、日志意义减弱。
原文档用餐厅点餐的例子对比。事件式:一次 dispatch 描述发生了什么——
{ type: "food/orderAdded", payload: {pizza: 1, coke: 1} }
Setter 式:客户端必须知道 state 结构并逐个"命令"设值——
{
type: "orders/setPizzasOrdered",
payload: { amount: getState().orders.pizza + 1 }
}
{
type: "orders/setCokesOrdered",
payload: { amount: getState().orders.coke + 1 }
}
"事件"只需要一个 action,更灵活:不关心之前订了多少披萨,也许没有厨师可用,订单可以被忽略。"setter"则要求客户端代码了解 state 的实际结构、"正确"的值是多少,最终要 dispatch 多个 action 才能完成一笔"事务"。
16. 写有意义的 Action 名称
action.type 有两个主要用途:reducer 逻辑检查 action type 决定是否处理;action type 会显示在 Redux DevTools 历史日志中供开发者阅读。实际 type 字段内容对 Redux 本身无意义,但对你(开发者)很重要——action 应写上有意义、信息丰富、可描述的 type 字段。理想情况下,浏览一份已 dispatch 的 action type 列表就能大致理解应用里发生了什么,无需查看每个 action 的内容。避免 "SET_DATA"、"UPDATE_STORE" 这类过于笼统的命名。
17. 允许多个 Reducer 响应同一个 Action
Redux reducer 逻辑的设计意图就是拆分为许多小 reducer,各自独立更新自己的 state 树部分,再组合成根 reducer。某个 action dispatch 时,可能被全部、部分或没有 reducer 处理。指南鼓励尽可能让多个 reducer 函数分别处理同一个 action。实践中经验表明多数 action 通常只被单个 reducer 处理,这没问题;但"事件式建模 + 多 reducer 响应"通常让代码库更易扩展,并减少为完成一次有意义更新而 dispatch 多个 action 的次数。
18. 避免连续 dispatch 大量 Action
避免连续 dispatch 大量 action 来完成一个更大的概念性"事务"。这在语法上合法,但通常会导致多次相对昂贵的 UI 更新,且中间状态可能被应用其他部分视为无效。应优先 dispatch 单个"事件"式 action,一次性产生所有适当的 state 更新;或者考虑使用 action batching 插件,让多个 action 只在最后触发一次 UI 更新。
原文档的解释与 Redux 源码机制完全对应:每次 dispatch 都会执行所有 store 订阅回调(通常每个连接的 UI 组件一个或多个)并通常引发 UI 更新。React 事件处理器内排队的更新通常被批处理为一次渲染,但在事件处理器之外(大多数 async 函数、timeout 回调、非 React 代码)排队的更新不会——那些场景下每次 dispatch 都会触发完整的同步 React 渲染路径,降低性能。此外,概念上属于一笔"事务"的多个 dispatch 会产生可能无效的中间状态:如连续 dispatch "UPDATE_A"、"UPDATE_B"、"UPDATE_C",若某处代码期望 a、b、c 三者一起更新,则前两次 dispatch 之后的 state 实质上是不完整的。确实需要多次 dispatch 时,可考虑批量方式:批处理 React 自身渲染(如 React-Redux 的 batch())、对 store 通知回调做防抖、或把多个 action 归组为只产生一次订阅通知的大 dispatch。更多示例见 FAQ 中"如何减少 store 更新事件"(docs/faq/Performance.md)。
createStore 的 dispatch() 结尾正是这个"每个 action 触发一次全体订阅者通知"机制的实现:
const listeners = (currentListeners = nextListeners)
listeners.forEach(listener => {
listener()
})
return action
19. 评估每段 State 应该放在哪里
"Redux 三原则"说"整个应用的状态存储在一棵 state 树中",这句话被过度解读了。它并不意味着应用中的每个值都必须放进 Redux store,而是应有一个地方可以找到你认为全局的、应用级的所有值;"局部"的值应放在最近的 UI 组件中。因此由你(开发者)决定什么 state 该进 Redux store、什么留在组件 state。指南指出的决策规则:docs/faq/OrganizingState.md 中"Do I Have to Put All My State into Redux?"一节。
20. 使用 React-Redux 的 Hooks API
优先使用 React-Redux hooks API(useSelector 与 useDispatch)作为从 React 组件访问 Redux store 的默认方式。经典 connect API 依然可用且会继续支持,但 hooks 在多个方面通常更易用:间接层更少、代码更少、与 TypeScript 配合比 connect 简单。hooks API 在性能与数据流上带来与 connect 不同的取舍,但官方现在推荐其作为默认。
原文档解释了两者的本质差异:经典 connect 是 HOC(高阶组件),它生成一个包装组件去订阅 store、渲染你自己的组件并向下传 store 数据与 action creators 作为 props——这是刻意的间接层,让你能写"展示型"组件。hooks 改变了大多数 React 开发者的写法:组件负责通过调用合适的 hook 自行请求数据。connect 的间接层一直让部分用户难以追踪数据流,其复杂性(多重载、可选参数、mapState/mapDispatch/父组件 props 合并、action creators 与 thunks 绑定)也使得用 TypeScript 正确标注非常困难。useSelector/useDispatch 消除了间接层,数据流一目了然;由于 useSelector 只接受单个 selector,TypeScript 标注也简单得多。
21. 让更多组件连接 Store 读取数据
倾向于让更多 UI 组件以更细的粒度订阅 Redux store。这通常带来更好的 UI 性能:某段 state 变化时,需要渲染的组件更少。例如不要只连接 <UserList> 读整个 users 数组;而是让 <UserList> 取所有用户 ID 列表、渲染 <UserListItem userId={userId}>,由 <UserListItem> 自己连接 store 并提取自己的用户条目。此条对 connect() 与 useSelector() 均适用。
22. connect 的 mapDispatch 使用对象简写形式
connect 的 mapDispatch 参数可以写成接收 dispatch 的函数,也可以是包含 action creators 的对象。指南建议始终使用"对象简写"形式,它显著简化代码;几乎从不真正需要把 mapDispatch 写成函数。
23. 在函数组件中多次调用 useSelector
用 useSelector 取数据时,优先多次调用、每次取较小量的数据,而不是用一个大的 useSelector 返回包含多个结果的对象。与 mapState 不同,useSelector 不要求返回对象,selector 读更小的值意味着某次 state 变化导致该组件重渲染的概率更低。但也要把握粒度平衡:如果单个组件确实需要某切片的全部字段,就写一个 useSelector 返回整个切片,而不必为每个字段单独写 selector。
24. 使用静态类型
使用 TypeScript 或 Flow 等静态类型系统,而非纯 JavaScript。类型系统能捕获许多常见错误、改善代码文档性、提升长期可维护性。Redux 与 React-Redux 最初按纯 JS 设计,但两者与 TS/Flow 都配合良好;Redux Toolkit 本身就是用 TS 编写的,设计上以极少的额外类型声明提供良好的类型安全。本仓库核心即为 TypeScript 源码(src/types/store.ts、src/types/reducers.ts 等),并配有专门的类型测试(test/typescript/store.test-d.ts);官方示例也同时提供 JS 版(examples/counter)与 TS 版(examples/counter-ts)。
25. 使用 Redux DevTools 扩展调试
配置你的 Redux store 以启用 Redux DevTools 扩展调试。它允许你查看:已 dispatch action 的历史日志、每个 action 的内容、action 后的最终 state、action 后的 state diff、以及显示 action 实际 dispatch 位置的函数调用栈。此外 DevTools 支持"时间旅行调试",在 action 历史中前后步进,查看应用不同时间点的完整 state 与 UI。Redux 正是为支持这种调试方式而设计的,DevTools 是使用 Redux 最有力的理由之一。
real-world 示例展示了经典集成方式——store 在 dev 环境经 compose 组合 DevTools.instrument() 增强器(见 examples/real-world/src/store/configureStore.dev.js):
const store = createStore(
rootReducer,
preloadedState,
compose(applyMiddleware(thunk, api, createLogger()), DevTools.instrument())
)
现代写法则由 configureStore 自动完成 DevTools 集成(examples/counter/src/app/store.js),无需手动 compose。
26. State 使用纯 JavaScript 对象
优先用纯 JS 对象和数组构建 state 树,而非 Immutable.js 等专用库。虽然 Immutable.js 有些潜在好处,但常被列举的目标(如廉价的引用比较)其实是不可变更新这一通用性质的属性,并不依赖特定库。用纯对象还能减小 bundle 体积、减少数据类型转换的复杂性。指南同时重申:若希望简化不可变更新逻辑,专门推荐(作为 Redux Toolkit 一部分的)Immer。
原文档详细拆解了 Immutable.js 常见理由与实践落差:
- 常见理由:廉价引用比较的性能提升、专用数据结构带来的更新性能、防意外修改、
setIn()式嵌套更新 API; - 实践结论:廉价引用比较是任何不可变更新的通用属性;防意外修改可用 Immer(消除易错的手动拷贝逻辑、开发环境默认 deep-freeze)或
redux-immutable-state-invariant(检查 state 修改)实现;Immer 让更新逻辑整体更简单,无需setIn();而 Immutable.js 本身 bundle 很大、API 复杂、API 会"感染"应用代码(所有逻辑必须知道在处理纯 JS 对象还是 Immutable 对象)、Immutable 对象转纯 JS 对象相对昂贵且总是产生全新的深层对象引用、且缺乏持续维护; - 剩余最强理由是非常大的对象(数万键级)的快速更新,大多数应用不会处理如此大的对象。总体结论:Immutable.js 以过多开销换过少的实际收益,Immer 是更好的选择。
Priority C 规则:推荐(一致性选择)
27. Action Type 写成 domain/eventName
早期 Redux 文档与示例普遍用 SCREAMING_SNAKE_CASE 定义 action type(如 "ADD_TODO"、"INCREMENT"),这符合多数语言的常量声明惯例,缺点是uppercase 字符串较难阅读。其他社区采用带"功能/域"前缀的变体,如 NgRx 的 "[Domain] Action Type"("[Login Page] Login")或 "domain:action"。RTK 的 createSlice 当前生成形如 "domain/action" 的 action type(如 "todos/addTodo")。指南建议今后采用 "domain/action" 惯例以提升可读性。
仓库示例正是此惯例的现成证据:examples/counter/src/features/counter/counterSlice.js 中 createAsyncThunk('counter/fetchCount', ...),slice 名 'counter' 即 domain 前缀,生成的 action type 为 counter/increment、counter/decrement、counter/fetchCount/pending 等。
28. 按 Flux Standard Action(FSA)约定写 Action
最初"Flux Architecture"文档只规定 action 对象须有 type 字段,未对字段种类或命名做进一步指引。为提供一致性,Redux 发展初期形成了"Flux Standard Actions"约定,要点为:
- action 的数据一律放进
payload字段; - 可有
meta字段携带附加信息; - 可有
error字段表示该 action 代表某种失败。
Redux 生态中许多库已采用 FSA 约定,Redux Toolkit 生成的 action creators 也符合 FSA 格式。建议优先使用 FSA 格式的 action 以保持统一。
注意(原文档说明的实践分歧):FSA 规范说"error" action 应设 error: true 并复用"合法"形式的 action type;但实践中多数开发者会为"成功"和"错误"各写独立 action type。两种方式均可接受。
29. 使用 Action Creators
"Action creator"函数源自最初的 Flux 方案。在 Redux 中 action creator 并非严格必需——组件或其他逻辑总可以 dispatch({type: "some/action"}) 内联书写 action 对象。但使用 action creator 能带来一致性,尤其在需要为 action 内容做准备工作或附加逻辑时(如生成唯一 ID)。建议对任何 action 的 dispatch 都使用 action creator;但不要手写,推荐用 RTK 的 createSlice,它会自动生成 action creators 与 action types。仓库示例展示了两种形态:手写 action creators(examples/todomvc/src/actions/index.js):
import * as types from '../constants/ActionTypes'
export const addTodo = text => ({ type: types.ADD_TODO, text })
export const deleteTodo = id => ({ type: types.DELETE_TODO, id })
export const completeAllTodos = () => ({ type: types.COMPLETE_ALL_TODOS })
export const clearCompleted = () => ({ type: types.CLEAR_COMPLETED })
export const setVisibilityFilter = filter => ({
type: types.SET_VISIBILITY_FILTER,
filter
})
以及 createSlice 自动生成(examples/counter/src/features/counter/counterSlice.js 末尾 export const { increment, decrement, incrementByAmount } = counterSlice.actions)。
30. 用 RTK Query 做数据获取
实践中,典型 Redux 应用里副作用最常见的单一用途就是从服务器获取并缓存数据。因此指南推荐使用 RTK Query 作为 Redux 应用数据获取与缓存的默认方案(入门见 docs/tutorials/essentials/part-7-rtk-query-basics.md)。RTK Query 的设计目标是正确管理按需从服务器取数、缓存、请求去重、组件更新等逻辑;几乎在所有场景下都不推荐手写数据获取逻辑。
31. 其他异步逻辑用 Thunks 与 Listeners
Redux 被设计为可扩展,middleware API 正是为了把不同形式的异步逻辑接入 store,用户不必被迫学习 RxJS 等特定库。这催生了大量 Redux 异步中间件插件,也带来了"该用哪个"的困惑。指南的立场:
- 命令式(imperative)逻辑——如需要访问
dispatch/getState的复杂同步逻辑、中等复杂度的异步逻辑(包括把逻辑移出组件)——推荐 Redux thunk 中间件(参见 docs/usage/writing-logic-thunks.mdx); - "响应式"逻辑——需要响应已 dispatch 的 action 或 state 变化、长时间运行的异步工作流、"后台线程"式行为——推荐 RTK 的 listener middleware(
createListenerMiddleware)。
指南不推荐在多数场景(尤其异步数据获取)使用更复杂的 Redux-Saga 与 Redux-Observable;仅当没有其他工具足够强大时才考虑。
32. 把复杂逻辑移出组件
传统建议是把尽可能多的逻辑移出组件——部分出于"容器/展示"模式的考虑,部分因为类组件生命周期方法中处理异步逻辑难以维护。指南依然鼓励把复杂的同步或异步逻辑移出组件,通常放进 thunks,尤其当逻辑需要读取 store state 时。但React hooks 的使用确实让在组件内部直接管理数据获取这类逻辑变得更容易,在某些场景可能取代 thunk 的必要性。
33. 用 Selector 函数读取 Store State
"Selector 函数"是封装从 Redux store 读取值、并基于这些值派生更多数据的强有力工具。Reselect 等库支持创建记忆化 selector 函数,仅在输入变化时重算结果,这是性能优化的重要一环。强烈建议尽可能使用记忆化 selector 函数读取 store state,推荐用 Reselect 创建这些 selector。但不要觉得每个 state 字段都必须写 selector 函数——基于字段被访问/更新的频率以及 selector 在你应用中带来的实际收益,找到合理的粒度平衡。
34. Selector 函数命名为 selectThing
建议以 select 为前缀命名 selector 函数,后接所选值的描述。例如 selectTodos、selectVisibleTodos、selectTodoById。仓库示例即遵循此约定:examples/counter/src/features/counter/counterSlice.js 导出 selectCount,examples/todomvc/src/selectors/index.js 提供 getVisibleTodos、getCompletedTodoCount 这类语义明确的派生选择器。
35. 避免把表单状态放进 Redux
大多数表单状态不应放进 Redux。多数场景下这些数据并非真正全局、不被缓存、也不被多个组件同时使用;此外把表单连接 Redux 常意味着每次 change 事件都 dispatch 一次 action,带来性能开销而无实际收益(你大概不需要从 name: "Mark" 时间旅行回退一个字符到 name: "Mar")。即使数据最终会进 Redux,也建议表单编辑本身留在本地组件状态,只在用户提交表单时 dispatch 一次 action 更新 store。确有适用场景——如编辑项属性的 WYSIWYG 实时预览——但多数情况下没有必要。
落地建议:把规则当作"检查清单"使用
风格指南的元规则值得最后强调:它是建议而非教条。A 级规则(不可变、reducer 纯函数、可序列化、单 store)直接对应 Redux 运行机制——createStore 的 dispatch 校验与 isDispatching 保护(src/createStore.ts)、combineReducers 的 reducer 形状断言(src/combineReducers.ts)在实现层强制或印证了其中多条;B 级规则(RTK + Immer、feature folder、逻辑入 reducer、状态机、最小化 state、hooks API、DevTools)覆盖了从代码组织到调试体验的完整链路,且仓库中 counter / counter-ts / todomvc / real-world 各示例(见 examples/README.md)可作为每条规则的参考实现;C 级规则(domain/event 命名、FSA、action creators、RTK Query、thunk/listener、selectThing 命名)则适合写入团队编码规范以保证一致性。对新项目,最省心的路径是:configureStore + createSlice + hooks + RTK Query,再按本清单逐条对照自查。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00