Redux 状态管理速查表 - Redux Toolkit 与 React 状态管理大全

这张速查给想在 React 里做可预测、集中式状态管理的开发者,用的是 Redux Toolkit 这套现代化写法。内容涵盖 store 配置、createSlice(自动生成 action、靠 Immer 保证不可变)、createAsyncThunk 及其 pending/fulfilled/rejected 三态处理、中间件,以及 React 侧的 useSelector/useDispatch 绑定。收尾章节用 createEntityAdapter 展示规范化实体管理,并顺带看一眼 RTK Query。读完你应能搭好一个 slice、同时派发同步与异步 action、响应式读取状态,并把集合建模成按 id 存储的规范化实体。

编程语言·共 37 条命令·最后更新 2026-07-21
reduxreact状态管理store

典型使用场景

面向正在把散落的 useState / Context 状态收进单一 store 的前端团队,尤其是从手写 redux 迁到 Redux Toolkit(RTK)的项目。RTK 真正的价值不在少写 action 常量,而在三处约定:reducer 必须是纯函数(Immer 帮你做不可变更新)、slice 的 name 自动成为 action 前缀、以及 createAsyncThunk 生成的 pending / fulfilled / rejected 三个 action 天然对应请求三态。本表覆盖 store 配置、slice、thunk、中间件、选择器与实体适配器,以及为什么 useSelector 必须返回稳定引用这条最容易踩的性能坑。

Store 配置 5

const store = configureStore({ reducer: rootReducer })
RTK 创建 store,自动带上 thunk 与 devTools 等默认配置
configureStore({ reducer: { users: usersReducer, posts: postsReducer } })
多 reducer 自动合并
configureStore({ middleware: (gdm) => gdm().concat(logger) })
追加自定义中间件
configureStore({ devTools: process.env.NODE_ENV !== "production" })
仅开发环境开启 devTools
const rootReducer = combineReducers({ users, posts })
手动合并多个 reducer

Slice 创建 6

const slice = createSlice({ name, initialState, reducers })
一次定义 name/initialState/reducers,自动生成同名 action 与 reducer
name: "counter"
slice 命名空间,自动作为生成的 action 前缀(如 counter/increment)
initialState: { count: 0 }
定义这个 slice 的初始值,reducer 首次运行从这里开始
reducers: { increment(state) { state.count += 1 } }
同步 reducer(Immer 自动不可变)
extraReducers: (builder) => builder.addCase(fulfilled, ...)
处理外部异步 action
export const { increment } = slice.actions
导出自动生成的 action creators

Actions 与 Dispatch 5

dispatch(increment())
dispatch 同步 action
dispatch(fetchUser(id))
dispatch 异步 thunk
const { increment } = counterSlice.actions
从 slice 获取 action creator
store.dispatch({ type: "counter/increment" })
直接 dispatch 原始 action 对象
export default slice.reducer
导出 reducer 给 store

异步处理 5

const fetchUser = createAsyncThunk("user/fetch", async (id) => {})
创建异步 thunk
builder.addCase(fetchUser.pending, (state) => {})
处理请求中状态
builder.addCase(fetchUser.fulfilled, (state, action) => {})
处理请求成功
builder.addCase(fetchUser.rejected, (state, action) => {})
处理请求失败
await dispatch(fetchUser(id)).unwrap()
获取成功结果或抛出错误

中间件 5

const logger = () => (next) => (action) => {}
自定义中间件签名(三层柯里化)
middleware: (gdm) => gdm().concat(customMiddleware)
保留默认中间件并追加
configureStore({ middleware: (gdm) => gdm({ serializableCheck: false }) })
配置默认中间件选项
import { thunk } from "redux-thunk"
redux-thunk 异步中间件(RTK 默认内置)
gdm({ immutableCheck: false, serializableCheck: false })
生产环境关闭检查以提升性能

选择器 5

useSelector((state) => state.counter.value)
订阅式读取状态,store 中对应值变化时组件自动重渲染
const dispatch = useDispatch()
在组件里拿到 dispatch,用它触发 action
const selectCount = (state) => state.counter.value
定义选择器函数
const selectUserById = createSelector(...)
创建 memoized 选择器(reselect)
useSelector(selectUserById, shallowEqual)
浅比较避免不必要的重渲染

常用模式 6

<Provider store={store}><App /></Provider>
在根组件注入 store
state.entities[id]
规范化状态(按 id 存储实体)
createEntityAdapter({ selectId: (item) => item.id })
实体适配器管理集合
adapter.getSelectors((state) => state.items)
获取实体适配器选择器
adapter.upsertOne(state, item)
存在则更新,不存在则插入
import { createApi } from "@reduxjs/toolkit/query/react"
RTK Query 数据获取

参数矩阵

参数作用示例
configureStore({ reducer })创建 store,自动内置 thunk 与 devTools 等默认中间件const store = configureStore({ reducer: rootReducer })
configureStore({ reducer: { a, b } })传对象时多个 reducer 自动合并成根状态configureStore({ reducer: { users: usersReducer, posts: postsReducer } })
createSlice({ name, initialState, reducers })一次定义 name / initialState / reducers,自动生成同名 action 与 reducerconst slice = createSlice({ name, initialState, reducers })
extraReducers: (builder) => ...在 slice 内响应其它 slice 或外部 thunk 的 actionextraReducers: (builder) => builder.addCase(fulfilled, ...)
createAsyncThunk(typePrefix, payloadCreator)创建异步 thunk,自动产生三个生命周期 actionconst fetchUser = createAsyncThunk("user/fetch", async (id) => {})
dispatch(thunk).unwrap()在组件里 await thunk,失败时直接抛出错误而非返回 rejected actionawait dispatch(fetchUser(id)).unwrap()
middleware: (gdm) => gdm().concat(custom)保留默认中间件并追加自定义中间件configureStore({ middleware: (gdm) => gdm().concat(logger) })
useSelector(selectorFn, shallowEqual)订阅式读取状态;用浅比较避免每次返回新对象导致的重渲染useSelector(selectUserById, shallowEqual)
createSelector(...)创建记忆化选择器,依赖不变时复用上次结果const selectUserById = createSelector(...)
adapter.getSelectors(state => state.items)取得实体适配器的选择器集合,配合规范化状态使用adapter.getSelectors((state) => state.items)

易错点与避坑指南

现象reducer 里写了 fetch / setTimeout / localStorage 写入,控制台警告「Unexpected side effect in reducer」,且状态时序错乱。

原因reducer 必须是纯函数,同样的输入必须得到同样的输出,副作用让 Redux DevTools 的时间旅行失效。

处置把这些副作用挪进 thunk 或组件层:const fetchUser = createAsyncThunk("user/fetch", async (id) => {}),reducer 只负责改 state。

现象在 Immer 管理的 state 上做了原地修改之外的操作,返回的还是同一个引用,组件不更新。

原因reducer 直接 return 了原 state 对象或某个未修改的子对象,引用没变,useSelector 认为状态未变化。

处置所有修改都要在 Immer draft 上写并 return state;跨 reducer 复用同一个对象引用时也要展开:state.count += 1 这类写法由 Immer 生成新引用。

现象useSelector(() => ({ a: state.a, b: state.b })) 导致每次 store 更新都重渲染。

原因选择器每次返回一个新对象引用,默认引用比较永远判定为「变了」。

处置改用浅比较:useSelector(selectUserById, shallowEqual),或把选择器抽出去用 createSelector 记忆化。

现象请求失败时页面没有任何错误提示,await dispatch(...) 也没抛错。

原因dispatch 一个 rejected 的 thunk 默认会 resolve 成一个 rejected action 对象,而不是抛出。

处置在调用处加 .unwrap():await dispatch(fetchUser(id)).unwrap(),用 try/catch 捕获;或读 action.payload 作为错误原因。

现象自定义中间件写完后完全没被调用,或报错说不该同时有 thunk。

原因middleware 被写成默认中间件之外的形式(漏了 gdm()),把 thunk 等默认中间件整体覆盖掉了。

处置写成 configureStore({ middleware: (gdm) => gdm().concat(customMiddleware) }),只追加不替换;仅在明确知道代价时用 gdm({ serializableCheck: false }) 调整选项。

现象开发环境控制台刷出 immutableCheck / serializableCheck 警告,页面却能正常工作。

原因state 里放了 Date、Map 或函数这类不可序列化或可变结构,检查器只是在提醒。

处置按提示把这类值移出 store(存时间戳字符串、id 列表),确有必要时再 gdm({ immutableCheck: false, serializableCheck: false })。

排障路径

  1. 1确认 store 装配与 slice 命名空间

    configureStore({ reducer: { users: usersReducer, posts: postsReducer } })

    每个 slice 的 name 会成为 action 前缀(如 counter/increment),排查 action 时先确认这个前缀。

  2. 2追踪一个异步请求的完整生命周期

    builder.addCase(fetchUser.pending, (state) => {})

    pending / fulfilled / rejected 三个 case 各加一行日志,即可确认请求到底有没有发出、走的哪个分支。

  3. 3定位组件为何频繁重渲染

    useSelector(selectUserById, shallowEqual)

    把选择器改成记忆化并加浅比较;若仍频繁重渲染,说明被订阅的切片本身变化过密,应下沉到更细的 slice。

  4. 4验证中间件链没有被覆盖

    configureStore({ middleware: (gdm) => gdm().concat(logger) })

    gdm() 先取出默认链再 concat 自己的中间件;漏掉 gdm() 会把 thunk 一并顶掉。

  5. 5在组件里拿到 dispatch 并直接派发原始 action

    store.dispatch({ type: "counter/increment" })

    调试时可以直接派发带 type 的原始对象验证 reducer,但业务代码应使用 slice 自动生成的 action creator。

命令示例

装配 store 并合并多个 reducer

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

传对象时多个 reducer 自动合并成根状态;configureStore 默认已带上 thunk 与 devTools 等中间件。

一个 slice 从定义到派发

const slice = createSlice({ name: "counter", initialState: { count: 0 }, reducers: { increment(state) { state.count += 1 } } }) export const { increment } = slice.actions export default slice.reducer dispatch(increment())

name 自动成为 action 前缀(counter/increment),reducers 里的原地赋值由 Immer 生成不可变更新。

异步 thunk 的三个生命周期分支

const fetchUser = createAsyncThunk("user/fetch", async (id) => {}) builder.addCase(fetchUser.pending, (state) => {}) builder.addCase(fetchUser.fulfilled, (state, action) => {}) builder.addCase(fetchUser.rejected, (state, action) => {}) await dispatch(fetchUser(id)).unwrap()

在组件里用 .unwrap() 才能让失败直接抛错;不加 .unwrap() 时 dispatch 返回的是 action 对象而不是结果。

在组件里读取状态并避免多余渲染

const dispatch = useDispatch() useSelector((state) => state.counter.value) useSelector(selectUserById, shallowEqual) const selectUserById = createSelector(...)

返回原始值最稳;必须返回对象时给 shallowEqual,或用 createSelector 做记忆化复用上次结果。

常见坑与注意事项

  • reducer 里绝不能出现 fetch、setTimeout、localStorage 写入等副作用,控制台会报「Unexpected side effect in reducer」且状态时序会错乱。
  • useSelector(() => ({ a: state.a, b: state.b })) 每次返回新引用会让组件反复重渲染,改用原始值选择器或加 shallowEqual。
  • await dispatch(thunk) 不加 .unwrap() 时失败只返回 rejected action、不抛错,错误必须自己在 rejected 分支里读。
  • middleware 必须写成 (gdm) => gdm().concat(...) ;直接传数组会覆盖默认链,而 RTK 已内置 redux-thunk,再加一份会报错。
  • 绕过 draft 改动状态(比如在别处持同一对象引用原地修改)会让引用不变,组件不会重渲染;实体集合请用 adapter.upsertOne。

提示

  • Redux 三大原则:单一数据源、状态只读、纯函数修改状态。
  • RTK 内置 Immer,可在 reducer 中直接"修改"状态,底层自动生成不可变更新。
  • useSelector 会在 store 更新时重新执行,返回新引用会触发重渲染,注意性能优化。
  • Redux Toolkit (RTK) 是官方推荐的方式,configureStore 和 createSlice 大幅简化样板代码。
  • createAsyncThunk 配合 extraReducers 处理异步,redux-thunk 已内置在 RTK 默认中间件中。

常见问题

createSlice 到底省掉了哪些样板代码?

省掉三块:手写的 action type 常量字符串、与 type 一一对应的 action creator 函数,以及 switch 风格的 reducer。用 createSlice({ name, initialState, reducers }) 写一次,name 会自动成为生成 action 的前缀,reducers 里的每个键同时产出一个同名 action creator(counter/increment、counter/increment),导出 const { increment } = slice.actions 即可派发。它不替你决定 state 的形状,也不减少 reducer 必须保持纯函数这条要求。

为什么在 Immer 管理的 state 上改了却没触发重渲染?

前提是这个改动确实是通过 Immer 的 draft 进行的。只要你没有走 createSlice 里那个受管的 state,而是把同一份对象在 reducer 之外又拿了一份引用去原地改,返回给 store 的还是旧引用,引用相等就不会通知订阅者。排查方法是把这个 action 单独派发一次看状态是否变化,若不变就说明修改发生在 draft 之外。createEntityAdapter({ selectId: (item) => item.id }) 管理的集合则要用 adapter.upsertOne(state, item) 这类方法更新。

createAsyncThunk 的三个 action 该在哪个组件处理?

pending / fulfilled / rejected 三个生命周期 action 都写进 slice 的 extraReducers,靠 builder.addCase 统一落到同一份状态上,这样「请求中」标志、结果、错误信息三处都不必散落在各个组件的本地 state 里。组件侧只负责派发:dispatch(fetchUser(id)),需要拿到返回值或让失败直接抛出时用 await dispatch(fetchUser(id)).unwrap()。把 fulfilled 的赋值逻辑留在组件里,就等于绕过了 extraReducers 的约定。

useSelector 为什么会引起不必要的重渲染?

因为 store 每次更新都会重新执行选择器函数,而你若在函数体里返回一个新对象(() => ({ a: state.a, b: state.b })),每次都是新引用,浅比较也判为不相等。两个办法:优先写成 useSelector((state) => state.counter.value) 这种返回原始值的形态;必须返回对象时给第二个参数传 shallowEqual,或者用 createSelector(...) 做记忆化,依赖不变时复用上次结果。需要跨 id 取数据时,adapter.getSelectors((state) => state.items) 提供的选择器已经做了这层处理。

自定义中间件怎么确认真的挂上了?

写 middleware: (gdm) => gdm().concat(customMiddleware),关键是先调用 gdm() 拿到默认中间件再 concat,直接传一个数组会覆盖默认链。RTK 默认已内置 redux-thunk(import { thunk } from "redux-thunk" 演示的就是这个中间件),所以自己再塞一个会报错。中间件签名是三层柯里化:const logger = () => (next) => (action) => {},最外层在 store 初始化时调用一次。确认方式是给它加一句控制台输出,派发一个同步 action 看有没有打出来。

官方参考来源

下方为命令对应的官方权威文档,供你核对最新用法与深入查阅。

由 巧匠 维护

公开更新于 2026年7月21日,内容持续校对官方文档。

联系我们

命令或描述有误?提交反馈、商务合作或产品建议都可发送邮件给我们。

联系我们