第99课:状态管理:Redux Toolkit(上)——Store、Slice、useSelector、useDispatch
当 React 应用的状态逻辑变得复杂——跨多个层级的组件需要共享和修改同一份数据、异步请求的状态需要在全局追踪——Context + useReducer 的组合开始显得力不从心。Redux 是 React 生态中最成熟、最广泛使用的全局状态管理库。然而传统 Redux 以样板代码多、配置繁琐著称:你需要手动编写 action creator、action type 常量、以及冗长的 switch-case reducer。Redux Toolkit(RTK) 是 Redux 官方推荐的“现代写法”,它将那些重复性工作封装为简洁的 API:createSlice 让你在一个函数里同时定义 state、reducers 和 actions;configureStore 一行代码创建 Store 并自动集成中间件和 DevTools;useSelector 和 useDispatch 在 React 组件中提供类型安全的读写接口。本节课将聚焦于 Redux Toolkit 的核心四件套,通过一个完整的计数器与购物车案例,展示从零搭建 Redux 状态管理层的正确姿势。
1. Redux Toolkit 的设计动机与传统 Redux 的痛点
1.1 传统 Redux 的样板代码问题
传统 Redux 需要手动创建以下分散的部分:
1 | // 1. Action Type 常量 |
即使只是一个简单的计数器,也需要跨越四个概念区域编写大量模板代码。随着应用规模增长,action type 字符串容易拼写错误、action creator 和 reducer 分散在不同文件中难以维护、不可变更新逻辑(...state)嵌套深时极易出错。
1.2 Redux Toolkit 的解决方案
Redux Toolkit 提供了以下核心 API,将上述样板代码缩减到几行:
- **
configureStore**:替代createStore,自动集成redux-thunk中间件、Redux DevTools、并预设了最佳实践配置。 - **
createSlice**:将 reducer 函数、action creator 和 action type 合并为一个“切片”(Slice)定义,无需手动编写 action type 常量。 - **
createAsyncThunk**:标准化异步逻辑(下节课详细讲解)。 - **
createSelector**:记忆化选择器(reselect的集成版本)。
Redux Toolkit 的核心理念:你只需关心“状态如何变化”(reducer 逻辑),action type 生成、action creator 创建、不可变更新(通过内置的 Immer 库)都由 RTK 自动处理。
2. configureStore:一行代码创建 Store
configureStore 是 Redux Toolkit 提供的 Store 创建函数。它封装了传统 Redux 的 createStore,并默认集成以下能力:
redux-thunk中间件(用于异步 dispatch)。- Redux DevTools Extension(浏览器调试工具)。
- 开发环境检查:检测意外状态突变(非不可变更新)和序列化问题。
1 | // store.js |
configureStore 接受一个配置对象,最核心的字段是 reducer。它可以是单个 reducer 函数(适用于简单应用),也可以是一个对象(推荐),将不同功能域的 reducer 映射到对应的 State 键名。
2.1 为 React 注入 Store
与 React 集成需要使用 react-redux 提供的 <Provider> 组件,将 Store 注入整个组件树:
1 | // main.jsx |
一旦 <Provider> 包裹了根组件,所有后代组件都可以通过 Hooks 访问 Store。
3. createSlice:Redux Toolkit 的灵魂
createSlice 将传统 Redux 中的 reducer 逻辑、action type 和 action creator 合三为一。它是 Redux Toolkit 使用频率最高的 API。
3.1 基本语法与自动 Action Creator
1 | // features/counter/counterSlice.js |
关键解析:
name: 'counter'决定了 action type 的前缀。例如incrementreducer 自动生成的 action type 是'counter/increment'。你无需手动定义常量。createSlice会自动为每个 reducer 生成对应的 action creator。counterSlice.actions.increment()返回{ type: 'counter/increment' }。counterSlice.actions.incrementByAmount(5)返回{ type: 'counter/incrementByAmount', payload: 5 }。- reducer 函数接收两个参数:
state(当前状态)和action(派发的 action 对象,action.payload携带数据)。
3.2 为什么可以直接“修改” state?内置 Immer 的秘密
你可能会惊讶:state.value += 1 看起来像是在直接修改现有 state。这与 Redux 的不可变性原则相矛盾!实际上,Redux Toolkit 内部集成了 Immer 库。Immer 使用 Proxy 对象跟踪你对 state 的所有修改,然后将这些修改转换为一个全新的、不可变的 state 对象。
- 你可以写“可变”代码:
state.value += 1、state.items.push(newItem)、state.user.name = 'Bob'——这些在传统 Redux 中都需要用扩展运算符层层克隆的写法,现在可以直接写。 - 你仍然必须遵循不可变原则:不能直接修改
action对象;不能有副作用(如setTimeout、fetch);reducer 内部必须是纯同步逻辑。 - 你也可以返回新对象:如果返回一个全新的对象(如
return { ...state, value: 0 }),Immer 会使用你返回的对象作为新状态。通常在重置整个 state 或完全替换数组时使用return。
3.3 将 Slice 注册到 Store
将 counterSlice.reducer 添加到 configureStore 的 reducer 映射中:
1 | // store.js |
此时,整个应用的全局 State 类型为 { counter: { value: number } }。
4. useSelector 与 useDispatch:在 React 组件中读写 Redux
React-Redux 提供了两个核心 Hook 来连接 React 组件与 Redux Store。
4.1 useSelector:从 Store 中读取数据
useSelector(selector) 接收一个选择器函数作为参数。该函数接收整个 Redux Store 的 state,返回你需要的任何派生数据。当组件渲染时,useSelector 会执行选择器函数;Redux 会比较返回值是否发生了变化(使用 === 严格相等),如果变化则触发组件重新渲染。
1 | import { useSelector } from 'react-redux'; |
关键规则:
- 选择器函数会在每次 Store 状态变化后执行(通过订阅机制)。
- 如果选择器返回值与上一次相同(
===),组件不会重新渲染。这就是 Redux 的“精细订阅”——组件只在它关心的数据变化时才重渲染。 - 避免在
useSelector中返回新对象:useSelector(state => ({ a: state.a, b: state.b }))每次都会返回一个新对象,导致组件每次 Store 变化都重渲染。如果需要多个字段,调用多次useSelector分别返回基本值,或使用shallowEqual比较。
1 | import { useSelector, shallowEqual } from 'react-redux'; |
4.2 useDispatch:向 Store 派发 Action
useDispatch 返回 Redux Store 的 dispatch 函数。你通过它来触发状态变更——调用从 createSlice 中导出的 action creator,并将返回的 action 对象传递给 dispatch。
1 | import { useDispatch } from 'react-redux'; |
工作流程总结:
- 用户点击按钮 → 调用
dispatch(incrementByAmount(5))。 - Redux 将
{ type: 'counter/incrementByAmount', payload: 5 }派发给 Store。 - Store 调用
counterSlice的 reducer,Immer 将state.value += 5转换为新的不可变 state。 - Store 通知所有订阅组件 state 已变更。
CounterDisplay中的useSelector检测到state.counter.value变化,触发组件重渲染。
5. 组合多个 Slice:购物车实战
下面通过一个包含“商品列表”和“购物车”两个 Slice 的示例,展示多 Slice 的组织方式。
1 | // features/products/productsSlice.js |
1 | // features/cart/cartSlice.js |
1 | // store.js |
1 | // components/ProductList.jsx |
1 | // components/Cart.jsx |
设计要点:
- 每个功能域(商品、购物车)拥有独立的 Slice 文件,reducer 和 action creator 就近管理。
- Store 配置中的
reducer对象的 key(products、cart)直接决定了全局 state 的结构,组件中的useSelector通过state.cart.items访问对应数据。 - 购物车添加商品时,如果已存在则增加数量,否则新增条目——Immer 使得这种“查找并修改”的逻辑如同操作普通 JavaScript 对象一样自然。
课后练习
一、概念自测(选择题 / 填空题)
(单选) Redux Toolkit 内部集成了哪个库来实现“可变”语法的不可变更新?
A. Ramda
B. Lodash
C. Immer
D. Immutable.js(单选)
createSlice自动生成的 action type 的命名规则是?
A. 随机生成唯一 ID。
B.{slice名称}/{reducer函数名}。
C. 由configureStore统一分配。
D. 需要手动指定。(填空) 在
configureStore的reducer对象中,key 的名称决定了______在全局 state 中的访问路径。(多选) 关于
useSelector的描述,哪些是正确的?
A. 它接收一个选择器函数,该函数返回组件需要的派生数据。
B. 如果选择器的返回值与上一次相同(===),组件不会重新渲染。
C. 可以在useSelector中直接修改 state 值。
D. 如果需要返回多个字段组成的对象,建议配合shallowEqual使用。
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要为一个 Todo 应用实现 Redux Toolkit 状态管理。要求如下:
- 使用
createSlice创建todosSlice,包含以下 reducer:addTodo(payload 为文本字符串,生成{ id, text, completed: false }添加到数组)、toggleTodo(根据 id 切换completed)、deleteTodo(根据 id 删除)、editTodo(根据 id 更新text)。 - 使用
configureStore配置 Store。 - 创建
TodoApp组件,包含输入框和添加按钮,使用useDispatch派发 action。 - 创建
TodoList组件,使用useSelector读取 todos 列表,渲染每一项(复选框切换完成状态、编辑按钮、删除按钮)。 - 使用
shallowEqual优化选择器(如需要返回对象时)。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 Redux Toolkit 代码。提示词中需明确指定 Slice 的 name、initialState 结构、所有 reducer 的签名和逻辑,以及组件如何使用 Hooks。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请为一个 React Todo 应用创建 Redux Toolkit 状态管理层。需要创建以下文件:
src/store.js:使用configureStore创建 Store,reducer对象包含todos: todosReducer。src/features/todos/todosSlice.js:使用createSlice,name: 'todos',initialState: []。定义 4 个 reducer:
addTodo(state, action):往数组 push{ id: Date.now(), text: action.payload, completed: false }。toggleTodo(state, action):通过 id 找到对应 todo,取反completed。deleteTodo(state, action):通过 id 过滤。editTodo(state, action):通过 id 找到对应 todo,更新text为action.payload.text。- 导出 actions 和 reducer。
src/App.jsx:使用<Provider store={store}>包裹。内部渲染<TodoApp />。src/components/TodoApp.jsx:useState管理输入框文本。useDispatch派发addTodo。渲染输入框 + 添加按钮 +<TodoList />。src/components/TodoList.jsx:useSelector读取state.todos。map渲染每个 todo:<input type="checkbox" checked={todo.completed}>+ 文本 + 编辑按钮 + 删除按钮。使用useDispatch派发toggleTodo、deleteTodo、editTodo(可使用prompt简易编辑)。- 所有代码使用 JSDoc 注释,确保可直接运行。完成后列出所有文件内容。
四、面试真题与参考答案
题目(百度前端面试题):
请详细说明 Redux Toolkit 相比传统 Redux 的优势,特别是
createSlice的设计解决了哪些痛点。在 Redux Toolkit 的 reducer 中,为什么可以编写“可变”语法的代码(如state.value += 1)?这违背了 Redux 的不可变原则吗?请解释其内部原理。
参考答案:
Redux Toolkit 相比传统 Redux 的核心优势在于消除样板代码和提升开发体验。传统 Redux 需要手动定义 action type 常量、编写 action creator 函数、在 reducer 中使用 switch-case 判断 type、并通过多层扩展运算符手动保持不可变更新。createSlice 将 action type 生成、action creator 创建和 reducer 逻辑合并为一个 API——你只需定义 reducer 函数,RTK 自动生成对应的 action creator 和 type,减少了约 70% 的代码量。
在 createSlice 的 reducer 中编写“可变”语法(如 state.value += 1)之所以不违背 Redux 不可变原则,是因为 Redux Toolkit 内部集成了 Immer 库。Immer 使用 JavaScript 的 Proxy 对象创建一个 state 的“草稿”(draft)。你在 reducer 中对草稿的所有修改(包括深层嵌套的修改、数组的 push/pop 等)都会被 Immer 追踪,最终 Immer 将这些修改转换为一个全新的、不可变的 state 对象。你看到的“可变”代码只是语法层面的便利,底层仍然产出了不可变的数据。createSlice 的 reducer 会自动启用 Immer,无需额外配置。这也解决了深层嵌套对象的不可变更新代码极其冗长且易出错的问题——使用 Immer 后,无论嵌套多深,更新代码都与直接修改变量一样直观。
课后练习答案
一、概念自测答案
C
- 解析:Redux Toolkit 内置 Immer,使 reducer 中可以编写“可变”语法的代码。A、B 与 Redux Toolkit 无关;D 是另一个库,非 RTK 内置。
B
- 解析:
createSlice根据name字段和 reducer 函数名自动生成 action type,格式为{name}/{reducerName},如'counter/increment'。
- 解析:
对应的 reducer 管理的 state
- 解析:
configureStore({ reducer: { counter: counterReducer } })中,keycounter决定了访问路径state.counter。
- 解析:
A、B、D
- 解析:C 错误,
useSelector是只读的,不能修改 state。A、B、D 均为正确描述——shallowEqual避免对象选择器每次都返回新引用导致不必要的渲染。
- 解析:C 错误,
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请用 Redux Toolkit 实现一个 Todo 应用的状态管理。要求:
createSlice创建todosSlice:initialState 为空数组,reducers 包含addTodo(接收文本)、toggleTodo(接收 id)、deleteTodo(接收 id)、editTodo(接收 id 和 text)。configureStore配置 Store,挂载 todos reducer。TodoApp组件:输入框 + 添加按钮,使用useDispatch。TodoList组件:使用useSelector读取 todos,渲染列表,每项有复选框、文本、编辑和删除按钮。- 输出所有文件:store.js、todosSlice.js、App.jsx、TodoApp.jsx、TodoList.jsx。”