WinddSnow

Redux-Toolkit-Part1-Store-Slice-useSelector-useDispatch

字数统计: 4.4k阅读时长: 19 min
2026/08/01

第99课:状态管理:Redux Toolkit(上)——Store、Slice、useSelectoruseDispatch

当 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;useSelectoruseDispatch 在 React 组件中提供类型安全的读写接口。本节课将聚焦于 Redux Toolkit 的核心四件套,通过一个完整的计数器与购物车案例,展示从零搭建 Redux 状态管理层的正确姿势。


1. Redux Toolkit 的设计动机与传统 Redux 的痛点

1.1 传统 Redux 的样板代码问题

传统 Redux 需要手动创建以下分散的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. Action Type 常量
const INCREMENT = 'counter/increment';
const DECREMENT = 'counter/decrement';

// 2. Action Creator
function increment() { return { type: INCREMENT }; }
function decrement() { return { type: DECREMENT }; }

// 3. Reducer
const initialState = { value: 0 };
function counterReducer(state = initialState, action) {
switch (action.type) {
case INCREMENT: return { ...state, value: state.value + 1 };
case DECREMENT: return { ...state, value: state.value - 1 };
default: return state;
}
}

// 4. Store
const store = createStore(counterReducer);

即使只是一个简单的计数器,也需要跨越四个概念区域编写大量模板代码。随着应用规模增长,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
2
3
4
5
6
7
8
9
10
// store.js
import { configureStore } from '@reduxjs/toolkit';

const store = configureStore({
reducer: {
// 这里添加各个 Slice 的 reducer
},
});

export default store;

configureStore 接受一个配置对象,最核心的字段是 reducer。它可以是单个 reducer 函数(适用于简单应用),也可以是一个对象(推荐),将不同功能域的 reducer 映射到对应的 State 键名。

2.1 为 React 注入 Store

与 React 集成需要使用 react-redux 提供的 <Provider> 组件,将 Store 注入整个组件树:

1
2
3
4
5
6
7
8
9
10
11
12
// main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
);

一旦 <Provider> 包裹了根组件,所有后代组件都可以通过 Hooks 访问 Store。


3. createSlice:Redux Toolkit 的灵魂

createSlice 将传统 Redux 中的 reducer 逻辑action typeaction creator 合三为一。它是 Redux Toolkit 使用频率最高的 API。

3.1 基本语法与自动 Action Creator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
name: 'counter', // Slice 名称,用于生成 action type 前缀
initialState: { value: 0 }, // 初始状态
reducers: {
// 每个 reducer 函数都会自动生成对应的 action creator
increment(state) {
state.value += 1;
},
decrement(state) {
state.value -= 1;
},
incrementByAmount(state, action) {
state.value += action.payload;
},
reset(state) {
state.value = 0;
},
},
});

// 导出自动生成的 action creator
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;

// 导出 reducer
export default counterSlice.reducer;

关键解析

  • name: 'counter' 决定了 action type 的前缀。例如 increment reducer 自动生成的 action type 是 'counter/increment'。你无需手动定义常量。
  • createSlice 会自动为每个 reducer 生成对应的 action creatorcounterSlice.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 += 1state.items.push(newItem)state.user.name = 'Bob'——这些在传统 Redux 中都需要用扩展运算符层层克隆的写法,现在可以直接写。
  • 你仍然必须遵循不可变原则:不能直接修改 action 对象;不能有副作用(如 setTimeoutfetch);reducer 内部必须是纯同步逻辑。
  • 你也可以返回新对象:如果返回一个全新的对象(如 return { ...state, value: 0 }),Immer 会使用你返回的对象作为新状态。通常在重置整个 state 或完全替换数组时使用 return

3.3 将 Slice 注册到 Store

counterSlice.reducer 添加到 configureStorereducer 映射中:

1
2
3
4
5
6
7
8
9
10
11
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';

const store = configureStore({
reducer: {
counter: counterReducer, // key 决定了 state.counter 在全局状态中的位置
},
});

export default store;

此时,整个应用的全局 State 类型为 { counter: { value: number } }


4. useSelectoruseDispatch:在 React 组件中读写 Redux

React-Redux 提供了两个核心 Hook 来连接 React 组件与 Redux Store。

4.1 useSelector:从 Store 中读取数据

useSelector(selector) 接收一个选择器函数作为参数。该函数接收整个 Redux Store 的 state,返回你需要的任何派生数据。当组件渲染时,useSelector 会执行选择器函数;Redux 会比较返回值是否发生了变化(使用 === 严格相等),如果变化则触发组件重新渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { useSelector } from 'react-redux';

function CounterDisplay() {
const count = useSelector(state => state.counter.value);
const isEven = useSelector(state => state.counter.value % 2 === 0);

return (
<div>
<p>当前计数:{count}</p>
<p>{isEven ? '偶数' : '奇数'}</p>
</div>
);
}

关键规则

  • 选择器函数会在每次 Store 状态变化后执行(通过订阅机制)。
  • 如果选择器返回值与上一次相同(===),组件不会重新渲染。这就是 Redux 的“精细订阅”——组件只在它关心的数据变化时才重渲染。
  • 避免在 useSelector 中返回新对象useSelector(state => ({ a: state.a, b: state.b })) 每次都会返回一个新对象,导致组件每次 Store 变化都重渲染。如果需要多个字段,调用多次 useSelector 分别返回基本值,或使用 shallowEqual 比较。
1
2
3
4
5
6
7
8
9
10
11
import { useSelector, shallowEqual } from 'react-redux';

// ✅ 分别选择基本值
const count = useSelector(state => state.counter.value);
const status = useSelector(state => state.counter.status);

// ✅ 使用 shallowEqual 让对象选择器正常工作
const { value, status } = useSelector(state => ({
value: state.counter.value,
status: state.counter.status,
}), shallowEqual);

4.2 useDispatch:向 Store 派发 Action

useDispatch 返回 Redux Store 的 dispatch 函数。你通过它来触发状态变更——调用从 createSlice 中导出的 action creator,并将返回的 action 对象传递给 dispatch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount } from './counterSlice';

function CounterControls() {
const dispatch = useDispatch();

return (
<div>
<button onClick={() => dispatch(increment())}>+1</button>
<button onClick={() => dispatch(decrement())}>-1</button>
<button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
</div>
);
}

工作流程总结

  1. 用户点击按钮 → 调用 dispatch(incrementByAmount(5))
  2. Redux 将 { type: 'counter/incrementByAmount', payload: 5 } 派发给 Store。
  3. Store 调用 counterSlice 的 reducer,Immer 将 state.value += 5 转换为新的不可变 state。
  4. Store 通知所有订阅组件 state 已变更。
  5. CounterDisplay 中的 useSelector 检测到 state.counter.value 变化,触发组件重渲染。

5. 组合多个 Slice:购物车实战

下面通过一个包含“商品列表”和“购物车”两个 Slice 的示例,展示多 Slice 的组织方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// features/products/productsSlice.js
import { createSlice } from '@reduxjs/toolkit';

const productsSlice = createSlice({
name: 'products',
initialState: {
items: [
{ id: 1, name: '笔记本电脑', price: 5999 },
{ id: 2, name: '无线耳机', price: 899 },
{ id: 3, name: '机械键盘', price: 499 },
],
},
reducers: {}, // 商品列表通常是静态数据,无需修改
});

export default productsSlice.reducer;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// features/cart/cartSlice.js
import { createSlice } from '@reduxjs/toolkit';

const cartSlice = createSlice({
name: 'cart',
initialState: {
items: [], // { id, name, price, quantity }
},
reducers: {
addItem(state, action) {
const { id, name, price } = action.payload;
const existing = state.items.find(item => item.id === id);
if (existing) {
existing.quantity += 1; // Immer 允许直接修改
} else {
state.items.push({ id, name, price, quantity: 1 });
}
},
removeItem(state, action) {
const id = action.payload;
state.items = state.items.filter(item => item.id !== id);
},
updateQuantity(state, action) {
const { id, quantity } = action.payload;
const item = state.items.find(item => item.id === id);
if (item) {
item.quantity = quantity;
if (item.quantity <= 0) {
state.items = state.items.filter(i => i.id !== id);
}
}
},
clearCart(state) {
state.items = [];
},
},
});

export const { addItem, removeItem, updateQuantity, clearCart } = cartSlice.actions;
export default cartSlice.reducer;
1
2
3
4
5
6
7
8
9
10
11
12
13
// store.js
import { configureStore } from '@reduxjs/toolkit';
import productsReducer from './features/products/productsSlice';
import cartReducer from './features/cart/cartSlice';

const store = configureStore({
reducer: {
products: productsReducer,
cart: cartReducer,
},
});

export default store;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// components/ProductList.jsx
import { useSelector, useDispatch } from 'react-redux';
import { addItem } from '../features/cart/cartSlice';

function ProductList() {
const products = useSelector(state => state.products.items);
const dispatch = useDispatch();

return (
<div>
<h2>商品列表</h2>
{products.map(p => (
<div key={p.id} style={{ marginBottom: 12 }}>
<span>{p.name} - ¥{p.price}</span>
<button onClick={() => dispatch(addItem(p))}>加入购物车</button>
</div>
))}
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// components/Cart.jsx
import { useSelector, useDispatch } from 'react-redux';
import { removeItem, updateQuantity, clearCart } from '../features/cart/cartSlice';

function Cart() {
const cartItems = useSelector(state => state.cart.items);
const dispatch = useDispatch();

const total = cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0);

return (
<div>
<h2>购物车 ({cartItems.length} 种商品)</h2>
{cartItems.length === 0 ? (
<p>购物车是空的</p>
) : (
<ul>
{cartItems.map(item => (
<li key={item.id}>
{item.name} - ¥{item.price} × {item.quantity}
<button onClick={() => dispatch(updateQuantity({ id: item.id, quantity: item.quantity + 1 }))}>+</button>
<button onClick={() => dispatch(updateQuantity({ id: item.id, quantity: item.quantity - 1 }))}>-</button>
<button onClick={() => dispatch(removeItem(item.id))}>删除</button>
</li>
))}
</ul>
)}
<p>合计:¥{total}</p>
{cartItems.length > 0 && (
<button onClick={() => dispatch(clearCart())}>清空购物车</button>
)}
</div>
);
}

设计要点

  • 每个功能域(商品、购物车)拥有独立的 Slice 文件,reducer 和 action creator 就近管理。
  • Store 配置中的 reducer 对象的 key(productscart)直接决定了全局 state 的结构,组件中的 useSelector 通过 state.cart.items 访问对应数据。
  • 购物车添加商品时,如果已存在则增加数量,否则新增条目——Immer 使得这种“查找并修改”的逻辑如同操作普通 JavaScript 对象一样自然。

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) Redux Toolkit 内部集成了哪个库来实现“可变”语法的不可变更新?
    A. Ramda
    B. Lodash
    C. Immer
    D. Immutable.js

  2. (单选) createSlice 自动生成的 action type 的命名规则是?
    A. 随机生成唯一 ID。
    B. {slice名称}/{reducer函数名}
    C. 由 configureStore 统一分配。
    D. 需要手动指定。

  3. (填空)configureStorereducer 对象中,key 的名称决定了 ______ 在全局 state 中的访问路径。

  4. (多选) 关于 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 状态管理层。需要创建以下文件:

  1. src/store.js:使用 configureStore 创建 Store,reducer 对象包含 todos: todosReducer
  2. src/features/todos/todosSlice.js:使用 createSlicename: '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,更新 textaction.payload.text
    • 导出 actions 和 reducer。
  3. src/App.jsx:使用 <Provider store={store}> 包裹。内部渲染 <TodoApp />
  4. src/components/TodoApp.jsxuseState 管理输入框文本。useDispatch 派发 addTodo。渲染输入框 + 添加按钮 + <TodoList />
  5. src/components/TodoList.jsxuseSelector 读取 state.todosmap 渲染每个 todo:<input type="checkbox" checked={todo.completed}> + 文本 + 编辑按钮 + 删除按钮。使用 useDispatch 派发 toggleTododeleteTodoeditTodo(可使用 prompt 简易编辑)。
  6. 所有代码使用 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 后,无论嵌套多深,更新代码都与直接修改变量一样直观。


课后练习答案

一、概念自测答案

  1. C

    • 解析:Redux Toolkit 内置 Immer,使 reducer 中可以编写“可变”语法的代码。A、B 与 Redux Toolkit 无关;D 是另一个库,非 RTK 内置。
  2. B

    • 解析:createSlice 根据 name 字段和 reducer 函数名自动生成 action type,格式为 {name}/{reducerName},如 'counter/increment'
  3. 对应的 reducer 管理的 state

    • 解析:configureStore({ reducer: { counter: counterReducer } }) 中,key counter 决定了访问路径 state.counter
  4. A、B、D

    • 解析:C 错误,useSelector 是只读的,不能修改 state。A、B、D 均为正确描述——shallowEqual 避免对象选择器每次都返回新引用导致不必要的渲染。

二、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。”
CATALOG
  1. 1. 第99课:状态管理:Redux Toolkit(上)——Store、Slice、useSelector、useDispatch
    1. 1.1. 1. Redux Toolkit 的设计动机与传统 Redux 的痛点
      1. 1.1.1. 1.1 传统 Redux 的样板代码问题
      2. 1.1.2. 1.2 Redux Toolkit 的解决方案
    2. 1.2. 2. configureStore:一行代码创建 Store
      1. 1.2.1. 2.1 为 React 注入 Store
    3. 1.3. 3. createSlice:Redux Toolkit 的灵魂
      1. 1.3.1. 3.1 基本语法与自动 Action Creator
      2. 1.3.2. 3.2 为什么可以直接“修改” state?内置 Immer 的秘密
      3. 1.3.3. 3.3 将 Slice 注册到 Store
    4. 1.4. 4. useSelector 与 useDispatch:在 React 组件中读写 Redux
      1. 1.4.1. 4.1 useSelector:从 Store 中读取数据
      2. 1.4.2. 4.2 useDispatch:向 Store 派发 Action
    5. 1.5. 5. 组合多个 Slice:购物车实战
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.6.3. 三、Agent 模式下的提示词示例
      4. 1.6.4. 四、面试真题与参考答案
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)