Redux-Toolkit状态管理与购物车实战

105 阅读34分钟

Redux Toolkit 现代状态管理深度解析:从原理到购物车实战

导读:本文系统讲解 Redux 的设计哲学与核心三原则,深入剖析 Redux Toolkit(RTK)的底层机制——包括 Immer 的 Proxy 拦截原理、createSlice 的代码生成逻辑、createAsyncThunk 的三态状态机,以及 createSelector 的记忆化算法。全文附完整可运行的购物车交互演示(纯 HTML/JS),并提供 TypeScript 集成最佳实践、高频面试考点精讲。适合已掌握 React 基础、希望在生产级项目中正确使用状态管理的前端开发者。


目录


零、导读与学习价值

0.1 核心名词速查

术语一句话解释
StoreRedux 的全局状态容器,整个应用只有一个
StateStore 中存储的状态树对象
Action描述"发生了什么"的普通 JS 对象,必须有 type 字段
Reducer纯函数,接收旧 state 和 action,返回新 state
Dispatch派发 action 的函数,是触发状态变更的唯一入口
Selector从 state 中提取派生数据的函数
SliceRTK 中将 action + reducer 合并为一个模块的概念
Immer通过 ES6 Proxy 实现"写时复制"的不可变数据库
Thunk一种 Redux 中间件,允许 dispatch 函数而非对象(用于异步)
createSliceRTK API,自动生成 action type、action creator 和 reducer
createAsyncThunkRTK API,封装异步操作并自动生成 pending/fulfilled/rejected
createSelectorReselect 的记忆化 selector 工厂(RTK 已集成)
RootState从 store.getState 推断出的完整状态树类型
AppDispatch从 store.dispatch 推断出的 dispatch 函数类型
Redux Persist将 Redux state 持久化到 localStorage/sessionStorage 的库

0.2 为什么要掌握 Redux Toolkit

Redux 是 React 生态中历史最悠久、使用最广泛的状态管理方案,至今仍是大型团队协作项目的首选。掌握 RTK 有以下工程价值:

  • 职场硬通货:绝大多数中大型 React 项目(电商、ERP、管理后台)均使用 Redux 或 RTK,面试必考
  • 框架基础:理解 Redux 的 Flux 架构思想,对理解 Vuex、Pinia、Zustand 等其他状态管理方案有直接帮助
  • 调试能力:Redux DevTools 的时间旅行调试是其他方案难以匹敌的工程优势
  • TypeScript 友好:RTK 原生支持 TypeScript,与类型系统深度集成,是类型安全状态管理的最佳实践

一、多组件状态共享的困境与 Redux 的解题思路

1.1 名词解释

  • Props Drilling(属性穿透):父组件将数据通过 props 逐层传递给深层子组件,中间层组件不使用该数据却不得不"转发"的现象。
  • 状态提升(State Lifting):将多个兄弟组件共享的状态移到它们最近的公共祖先组件中管理。
  • 单向数据流:数据只能从父组件流向子组件(通过 props),子组件通过回调函数通知父组件变更。

1.2 Props Drilling 与状态提升的边界

React 的设计原则是单向数据流:状态由父组件持有,通过 Props 向下传递,子组件通过回调函数触发父组件更新。这个模型在浅层组件树中运行良好,但随着应用规模增大,出现三类典型困境:

困境一:Props Drilling

App(持有 user 状态)
  └── Layout
        └── Sidebar
              └── UserMenu
                    └── Avatar(真正需要 user 的组件)

中间三层(Layout、Sidebar、UserMenu)需要透传 user prop,即便它们自身完全不使用这个数据。每新增一层组件,就需要修改所有中间层的 props 定义,维护成本极高。

困境二:兄弟组件状态同步

购物车图标(在 Header 中)需要显示商品数量,商品列表页(在 Main 中)的"加入购物车"按钮修改购物车状态。二者分属不同分支,若将状态提升到公共祖先 App,则任何购物车更新都会导致整个 App 重渲染。

困境三:状态变更难以追踪

多个组件都能修改同一份状态时,出现 bug 后很难确定是哪个组件的哪次操作导致了状态异常,调试成本极高。

1.3 Redux 的核心思路

Redux 将应用状态抽取到组件树之外,存储在独立的全局 store 中。任何组件都可以直接"订阅"所需状态,也可以直接"派发"修改状态的动作,彻底绕开 Props 层级。

graph TD
    A[组件A - Header购物车图标] -->|useSelector| S[Redux Store<br/>全局状态]
    B[组件B - 商品列表页] -->|dispatch action| S
    C[组件C - 侧边栏] -->|useSelector| S
    S -->|state变化通知| A
    S -->|state变化通知| C

【代码注释】此图展示 Redux 的核心架构:Store 作为单一数据源居中,所有组件通过 useSelector 订阅状态、通过 dispatch 修改状态,组件之间不再需要直接通信。这消除了 Props Drilling,并使状态变更路径完全可追踪。市面应用:淘宝、京东等电商平台的购物车、用户信息、商品筛选条件等全局状态均采用类似架构。

【实战要点】

  • 经典应用场景:购物车(多页面共享)、用户登录信息(全局鉴权)、主题色/语言设置(全局配置)、通知消息列表(跨组件触发)。
  • 常见坑:不加甄别地把所有状态放入 Redux。表单输入值、弹窗开关、Tab 选中状态等 UI 局部状态放 useState 即可,放入 Redux 只会增加复杂度、降低组件可复用性。
  • 最佳实践:区分"UI 状态"(组件私有)和"业务状态"(跨组件共享);只将真正需要跨组件共享或需要持久化的业务状态放入 Redux。

【本章小结】

问题场景推荐方案
组件内部 UI 状态useState
父子组件少量共享状态提升 + props
跨模块共享配置useContext
全局业务状态(购物车、用户)Redux / RTK
服务器数据缓存React Query / RTK Query

【面试考点】 Q:React 中什么情况下应该引入 Redux? A:当应用出现以下特征时才考虑引入 Redux:(1)多个非父子关系的组件需要共享同一份数据;(2)状态变更逻辑复杂,需要明确的变更记录和调试能力;(3)需要状态持久化或时间旅行调试。对于简单的跨组件状态,useContext + useReducer 已足够,引入 Redux 会增加不必要的复杂度。判断标准是「这个状态的变更是否需要被多个独立模块感知」。


二、Redux 三大核心原则

2.1 名词解释

  • 单一数据源(Single Source of Truth):整个应用的状态存储在唯一一个 store 的对象树中。
  • State 只读(State is Read-Only):不能直接修改 state,唯一的修改方式是 dispatch 一个 action。
  • 纯函数 Reducer(Pure Function Reducer):Reducer 必须是纯函数——给定相同的输入,始终返回相同输出,无副作用。

2.2 原则详解与底层意义

原则一:单一数据源

整个应用的 state 存在于一个对象树中,且这个对象树只存在于唯一的 store 里。

这个约束带来三个工程收益:

  1. 全局可见性:Redux DevTools 可以随时查看完整的应用状态快照
  2. 调试便利:服务端渲染时可以直接将服务端的 state 序列化后注入客户端
  3. 状态一致性:不存在"哪个组件的状态才是真相"的歧义

原则二:State 只读

不能直接修改 store 中的状态,唯一的修改方式是 dispatch 一个描述"发生了什么"的 action 对象:

// 错误:直接修改 state(Redux 中绝对禁止)
store.getState().cart.items.push(newItem)  // ❌

// 正确:dispatch action
store.dispatch({ type: 'cart/addToCart', payload: newItem })  // ✅

【代码注释】action 是一个普通 JS 对象,type 字段描述操作类型(约定用 域/操作 格式),payload 携带操作所需数据。这个约束让每一次状态变更都对应一条明确的"操作记录",是 Redux DevTools 时间旅行调试的基础。市面应用:大型电商平台使用 action 日志来还原用户操作路径,辅助客诉排查。

原则三:纯函数 Reducer

Reducer 是一个纯函数:

// reducer 的本质:(previousState, action) => newState
function cartReducer(state = initialState, action) {
  switch (action.type) {
    case 'cart/addToCart':
      // 必须返回新对象,不能修改 state!
      return { ...state, items: [...state.items, action.payload] }
    default:
      return state
  }
}

【代码注释】纯函数的两个要求:(1)相同输入必须返回相同输出;(2)不产生副作用(不修改参数、不发起网络请求、不写全局变量)。正因为 reducer 是纯函数,Redux DevTools 才能"重放"任意历史 action 来复现状态——这是时间旅行调试的理论基础。

纯函数约束的代价是手写不可变更新极其繁琐:

// 手写深层嵌套的不可变更新
return {
  ...state,
  cart: {
    ...state.cart,
    items: state.cart.items.map(item =>
      item.id === id
        ? { ...item, quantity: item.quantity + 1 }
        : item
    )
  }
}

这正是 RTK 中 Immer 的价值所在(第四章详述)。

2.3 单向数据流可视化

在这里插入图片描述

sequenceDiagram
    participant U as 用户界面(View)
    participant D as dispatch()
    participant R as Reducer
    participant S as Store(State)
    
    U->>D: 用户点击&#34;加入购物车&#34;
    D->>R: dispatch({ type: 'cart/addToCart', payload: product })
    R->>R: 纯函数计算新 state(Immer 处理不可变性)
    R->>S: 返回新 state
    S->>U: 通知所有 useSelector 订阅者
    U->>U: 数据变化的组件重新渲染

【代码注释】这是 Redux 的经典单向数据流。关键点:数据流是单向的(View → Action → Reducer → Store → View),不存在反向数据流;Reducer 是同步纯函数,异步操作(网络请求)由 Thunk 中间件在 dispatch 和 Reducer 之间处理。市面应用:这个模式直接影响了 Vuex(Vue 的官方状态管理)和 Pinia 的设计,理解 Redux 数据流就理解了主流状态管理方案的共同范式。

【实战要点】

  • 经典应用场景:Redux DevTools 的"时间旅行"功能依赖单向数据流——因为每个 action 都是纯粹的意图描述,可以在任意时间点重放,复现历史状态。
  • 常见坑:在 Reducer 中执行异步操作(如 fetch)——Reducer 必须是同步纯函数,异步操作必须在 Thunk 或 Saga 中处理,否则状态更新时序不可预测。
  • 最佳实践:action 的 type 字段采用 域名/操作名 命名规范(如 cart/addToCart),便于 DevTools 中一眼识别操作归属。

【本章小结】

原则约束工程价值
单一数据源全局唯一 store状态全局可见、服务端渲染方便
State 只读只能 dispatch action状态变更可追踪、可回放
纯函数 Reducer无副作用、相同输入相同输出时间旅行调试、可测试性

记忆口诀:单源只读纯函数——单一数据源,状态只读,纯函数处理。

【面试考点】 Q:为什么 Redux 要求 Reducer 是纯函数? A:纯函数有两个关键特性:(1)相同输入永远返回相同输出;(2)无副作用。这两点共同保证了 Redux 的可预测性。因为纯函数不依赖外部状态,只依赖参数,所以给定任意历史 action 序列,可以从初始状态重放所有 action 精确还原到任意时间点的状态,这是时间旅行调试的理论基础。如果 Reducer 有副作用(比如直接修改传入的 state),重放就会产生不同结果,调试价值消失。

Q:Redux 的单向数据流与 React 的单向数据流有什么关系? A:React 组件层面的单向数据流(props 从父到子)和 Redux 的单向数据流(View → Action → Reducer → Store → View)是两个层面的设计,但思想一致:数据流方向固定,变更路径可追踪。Redux 把 React 的「组件级单向流」提升到了「应用级单向流」,使跨组件状态共享也具备可预测性。


三、Redux Toolkit 解决了什么问题

3.1 名词解释

  • 样板代码(Boilerplate):为实现一个功能必须重复编写的固定格式代码,本身不包含业务逻辑。
  • RTK(Redux Toolkit):Redux 官方推荐的最佳实践工具集,大幅减少样板代码。
  • Immer:基于 ES6 Proxy 的不可变数据库,让开发者以"可变"的写法生成不可变数据。
  • createSlice:RTK 的核心 API,将 action type、action creator、reducer 合并为一个"切片"。

3.2 传统 Redux 的痛点

以添加购物车商品为例,传统 Redux 需要编写以下全部代码:

// 1. 定义 action type 常量(防止字符串拼写错误)
const CART_ADD_ITEM = 'cart/addItem'
const CART_REMOVE_ITEM = 'cart/removeItem'
const CART_SET_QUANTITY = 'cart/setQuantity'

// 2. 编写 action creator 函数
function addItem(product) {
  return { type: CART_ADD_ITEM, payload: product }
}
function removeItem(id) {
  return { type: CART_REMOVE_ITEM, payload: id }
}
function setQuantity(id, quantity) {
  return { type: CART_SET_QUANTITY, payload: { id, quantity } }
}

// 3. 编写 reducer(手写不可变更新)
function cartReducer(state = { items: [] }, action) {
  switch (action.type) {
    case CART_ADD_ITEM: {
      const existing = state.items.find(i => i.id === action.payload.id)
      if (existing) {
        return {
          ...state,
          items: state.items.map(item =>
            item.id === action.payload.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          )
        }
      }
      return { ...state, items: [...state.items, { ...action.payload, quantity: 1 }] }
    }
    case CART_REMOVE_ITEM:
      return { ...state, items: state.items.filter(i => i.id !== action.payload) }
    case CART_SET_QUANTITY:
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: action.payload.quantity }
            : item
        )
      }
    default:
      return state
  }
}

【代码注释】这段传统 Redux 代码有三个明显问题:(1)action type 字符串需要手动管理常量,一旦拼写错误,调试极困难;(2)手写展开运算符(...state...state.items.map)的不可变更新极易出错,深层嵌套对象更是噩梦;(3)每个功能都需要重复这三步(常量→creator→reducer),纯机械劳动。市面上,这种写法在 2019 年前的 React 项目中极为普遍,维护成本是现代项目的 3-5 倍。

3.3 RTK 的解决方案对比

// RTK 版本:等价于上面全部传统代码
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

const cartSlice = createSlice({
  name: 'cart',
  initialState: { items: [] as ICartItem[] },
  reducers: {
    addItem(state, action: PayloadAction<IProduct>) {
      const existing = state.items.find(i => i.id === action.payload.id)
      if (existing) {
        existing.quantity += 1  // 直接"修改"!Immer 处理不可变性
      } else {
        state.items.push({ ...action.payload, quantity: 1 })  // 直接 push!
      }
    },
    removeItem(state, action: PayloadAction<number>) {
      state.items = state.items.filter(i => i.id !== action.payload)
    },
    setQuantity(state, action: PayloadAction<{ id: number; quantity: number }>) {
      const item = state.items.find(i => i.id === action.payload.id)
      if (item) item.quantity = action.payload.quantity
    }
  }
})

// 自动生成的 action creators:cartSlice.actions.addItem / removeItem / setQuantity
// 自动生成的 action types:'cart/addItem' / 'cart/removeItem' / 'cart/setQuantity'
export const { addItem, removeItem, setQuantity } = cartSlice.actions
export default cartSlice.reducer

【代码注释】RTK 版本将传统 Redux 约 60 行代码压缩到 25 行,且几乎没有样板代码。关键改进:(1)name + reducerKey 自动生成 action type,无需手写常量;(2)Immer 让 reducer 可以直接"修改" state,告别层层展开;(3)action creator 从 reducer key 自动推断,无需手动编写。市面应用:2020 年后新建的 React 项目几乎全部使用 RTK,Redux 官方文档也明确推荐 RTK 作为唯一正式写法。

graph LR
    A[传统Redux] -->|大量样板代码| B[action type常量]
    A --> C[action creator函数]
    A --> D[switch/case reducer]
    A --> E[手写不可变更新]
    
    F[RTK] -->|createSlice一键生成| G[自动action type]
    F --> H[自动action creator]
    F --> I[简洁reducer写法]
    F -->|Immer内置| J[直接mutate写法]

【代码注释】此图对比传统 Redux 和 RTK 的代码量分布。RTK 把左侧四个独立概念合并成一个 createSlice 调用,Immer 消除了手写不可变更新的负担。这不只是代码量减少,更是认知负担的大幅降低。

【实战要点】

  • 经典应用场景:RTK 是 2020 年后 React 生态的标配,GitHub、Figma、Notion 等产品的 React 状态管理均已迁移到类 RTK 方案。
  • 常见坑:仍然在 RTK 的 reducer 中手写展开运算符(...state)——这是多余的,Immer 已经处理不可变性,直接 state.xxx = yyy 即可;但要注意,如果想替换整个 state 对象,必须 return 新对象而不是修改 state
  • 最佳实践:升级到 RTK 后,先迁移 reducer 写法,再逐步替换 action type 常量,最后删除手写的 action creator。不要混用旧写法和 RTK,会造成维护混乱。

【本章小结】

维度传统 ReduxRedux Toolkit
Action Type手写字符串常量name/key 自动生成
Action Creator手写函数自动生成
Reducerswitch/case对象方法
不可变更新手写展开运算符Immer 内置,直接 mutate
异步处理手动配置中间件createAsyncThunk 内置
DevTools手动配置configureStore 自动集成
代码量多(样板代码多)少(逻辑代码为主)

【面试考点】 Q:Redux Toolkit 相比传统 Redux 解决了哪些问题? A:三个核心问题:(1)样板代码——createSlice 自动生成 action type 和 action creator,消除手写常量和 switch/case;(2)不可变更新——内置 Immer,允许在 reducer 中直接 "mutate" state,底层自动生成新对象;(3)异步处理——createAsyncThunk 封装异步操作,自动生成 pending/fulfilled/rejected 三个 action,无需手动配置 redux-thunk。此外,configureStore 自动集成 DevTools 和常用中间件,大幅降低配置成本。


四、createSlice 底层原理深度解析

4.1 名词解释

  • ActionCreatorMap:createSlice 内部维护的映射表,将 reducer key 映射到对应的 action creator 函数。
  • Proxy(ES6):ES6 引入的元编程机制,可以拦截对象的读写操作,是 Immer 的实现基础。
  • Draft(草稿对象):Immer 创建的 Proxy 包装对象,对 draft 的所有写操作都被拦截记录。
  • 结构共享(Structural Sharing):生成新对象时,未修改的子树复用原对象的引用,而非深拷贝,是不可变数据的性能优化策略。
  • Produce:Immer 的核心函数,接收 baseState 和 recipe 函数,返回新的不可变 state。

4.2 createSlice 的代码生成机制

在这里插入图片描述

createSlice 在运行时(不是编译时)执行以下逻辑:

// createSlice 的简化实现(概念示意)
function createSlice({ name, initialState, reducers }) {
  const actionCreators = {}
  const sliceReducers = {}

  // 遍历 reducers 对象的每个 key
  Object.keys(reducers).forEach(key => {
    // 1. 生成 action type:格式为 "name/key"
    const type = `${name}/${key}`

    // 2. 生成 action creator 函数
    actionCreators[key] = (payload) => ({ type, payload })

    // 3. 注册 reducer(用 Immer 包装,允许直接 mutate)
    sliceReducers[type] = (state, action) =>
      produce(state, draft => {
        reducers[key](draft, action)  // 传入 draft,不是真实 state
      })
  })

  // 4. 组合 reducer
  function reducer(state = initialState, action) {
    const caseReducer = sliceReducers[action.type]
    return caseReducer ? caseReducer(state, action) : state
  }

  return { actions: actionCreators, reducer }
}

【代码注释】这段伪代码揭示了 createSlice 的核心:它是一个代码生成器,在 JS 运行时通过遍历 reducers 对象的键,动态创建 action creator 和 reducer 分支。关键点:传给 reducer 函数的 state 参数实际上是 Immer 的 draft 对象,不是真实 state;produce 函数负责将对 draft 的修改转化为新的不可变对象。市面应用:Pinia(Vue 3 状态管理)的 defineStore 采用了非常相似的自动化设计。

4.3 Immer 的 Proxy 拦截原理

Immer 是 RTK 最核心的底层依赖,理解它是理解"为什么可以直接 mutate state"的关键。

在这里插入图片描述

sequenceDiagram
    participant R as Reducer函数
    participant P as Immer.produce()
    participant D as Draft(ES6 Proxy)
    participant B as baseState(原始对象)
    
    R->>P: produce(baseState, recipe)
    P->>D: 用 Proxy 包装 baseState 创建 draft
    P->>R: 将 draft 传给 recipe 函数
    R->>D: recipe执行:state.items.push(...)
    D->>D: Proxy 拦截写操作,记录变更路径
    P->>P: recipe执行完毕,分析变更路径
    P->>B: 未修改的节点:直接复用原引用(结构共享)
    P->>P: 已修改的节点:创建新对象
    P->>R: 返回新的不可变 state

【代码注释】Immer 的工作分四步:(1)用 ES6 Proxy 代理 baseState 创建 draft;(2)将 draft 传给 recipe 函数(即我们写的 reducer);(3)Proxy 拦截所有对 draft 的写操作,记录"哪条路径被修改了";(4)recipe 执行完毕后,Immer 基于记录的变更路径,只重建被修改的子树,未修改的子树直接复用原引用(结构共享)。市面应用:React 状态不可变性约束的痛点催生了 Immer,它现在也被 Zustand、MobX 等方案借鉴。

Proxy 拦截的关键代码(Immer 源码简化):

function createProxy(baseState) {
  const modified = {}        // 记录哪些路径被修改了
  const proxies = {}         // 缓存已创建的子 proxy

  return new Proxy(baseState, {
    get(target, prop) {
      // 读取时:如果有修改值返回修改值,否则返回原值(可能递归创建子 proxy)
      if (prop in modified) return modified[prop]
      if (typeof target[prop] === 'object' && target[prop] !== null) {
        // 递归代理嵌套对象,实现深层拦截
        if (!proxies[prop]) proxies[prop] = createProxy(target[prop])
        return proxies[prop]
      }
      return target[prop]
    },
    set(target, prop, value) {
      // 写入时:记录到 modified,不修改原 target
      modified[prop] = value
      return true
    }
  })
}

【代码注释】这段代码揭示了 Immer Proxy 的关键机制:set 拦截器把写操作记录到 modified 对象而非真实修改 targetget 拦截器在读取嵌套对象时递归创建子 Proxy 实现深层拦截。原始 baseState 全程未被触碰。市面应用:Vue 3 的响应式系统(reactive())同样基于 ES6 Proxy,但目的是追踪读取(依赖收集)而非追踪写入,与 Immer 用途不同但机制相通。

4.4 结构共享算法

Immer 生成新对象时使用结构共享(Structural Sharing),而非深拷贝整个 state:

原始 state:
{
  cart: { items: [A, B, C] },  ← 被修改(push D)
  user: { name: '张三' },       ← 未修改
  theme: 'light'                ← 未修改
}

Immer 生成的新 state:
{
  cart: { items: [A, B, C, D] },  ← 新对象(cart 和 items 数组都是新引用)
  user: ─────────────────────────── 复用原引用 ✓(user 对象引用不变)
  theme: ────────────────────────── 复用原引用 ✓(theme 字符串不变)
}

结构共享的性能意义:

  • state.user === newState.usertrue(引用相等)
  • useSelector(state => state.user) 的组件不会重渲染
  • 只有 state.cart !== newState.cart 的组件才重渲染

【实战要点】

  • 经典应用场景:结构共享是 Redux + React 性能优化的理论基础——useSelector 默认用引用相等(===)判断是否需要重渲染,结构共享保证了未修改的数据引用不变,避免无谓重渲染。
  • 常见坑:在 Immer reducer 中同时 mutate state 又 return 新值——Immer 规定只能二选一。如果 return 了新值,Immer 会用 return 的值作为新 state,对 draft 的所有 mutate 操作被忽略。
  • 最佳实践:替换整个数组时(如 filter 后),用 return 语句:return state.filter(...);修改数组中的某个元素时,直接 mutate:state.items[0].quantity += 1

【本章小结】

机制技术实现作用
Action 自动生成遍历 reducers key消除手写 action type 常量
Reducer 包装Immer.produce允许直接 mutate,自动生成新对象
Proxy 拦截ES6 Proxy set/get捕获所有写操作,不修改原对象
结构共享基于变更路径重建未修改的子树复用引用,优化性能

记忆口诀:生成类型,Proxy 拦截,共享结构——createSlice 自动生成类型,Immer 用 Proxy 拦截写操作,结构共享优化性能。

【面试考点】 Q:Immer 是如何实现"直接 mutate state"却不破坏不可变性的? A:Immer 使用 ES6 Proxy 创建 baseState 的代理对象 draft。Proxy 的 set 拦截器将所有写操作记录到内部的变更日志,而不实际修改 baseState;get 拦截器对嵌套对象递归创建子 Proxy,实现深层拦截。当 recipe 函数(reducer)执行完毕,Immer 根据变更日志重建被修改的子树,未修改的子树直接复用原引用(结构共享)。整个过程 baseState 始终不变,最终返回一个新的不可变对象。

Q:createSlice 的 name 参数有什么作用? A:name 是 action type 的命名空间前缀。createSlice 会将 name 和 reducers 对象的每个 key 拼接为 name/key 格式的 action type(如 cart/addItem)。这个设计避免了不同 slice 之间的 action type 命名冲突,同时在 Redux DevTools 中可以一目了然地看到 action 的归属模块。


五、createAsyncThunk 异步三态状态机

5.1 名词解释

  • Thunk:一种特殊的函数,它延迟计算,是 Redux 异步中间件的核心概念。在 Redux 中,thunk 是一个返回函数的 action creator,被 dispatch 时由中间件拦截执行。
  • createAsyncThunk:RTK 提供的异步 thunk 工厂函数,自动生成 pending/fulfilled/rejected 三个 action。
  • pending:异步操作发起但尚未完成的状态(对应 Promise 的 pending 状态)。
  • fulfilled:异步操作成功完成的状态(对应 Promise 的 resolved 状态)。
  • rejected:异步操作失败的状态(对应 Promise 的 rejected 状态)。
  • extraReducers:createSlice 中专门处理外部 action(如 createAsyncThunk 生成的 action)的配置项。
  • rejectWithValue:createAsyncThunk 回调中提供的工具函数,将自定义错误信息携带在 rejected action 的 payload 中。

5.2 三态生命周期详解

在这里插入图片描述

stateDiagram-v2
    [*] --> idle: 初始状态
    idle --> pending: dispatch(fetchProducts())
    pending --> fulfilled: Promise resolved
    pending --> rejected: Promise rejected / rejectWithValue
    fulfilled --> pending: 再次 dispatch
    rejected --> pending: 重试
    fulfilled --> idle: clearState

【代码注释】此状态机图展示了 createAsyncThunk 的完整生命周期。关键点:pending 是请求发出但未完成的中间态,此时应在 UI 显示 loading 状态;fulfilled 携带成功数据(action.payload);rejected 携带错误信息(action.payload 通过 rejectWithValue 设置,或 action.error 为未捕获的错误对象)。市面应用:所有依赖网络请求的列表页(商品列表、用户列表、订单列表)都需要管理这三种状态。

createAsyncThunk 的内部实现逻辑(简化):

function createAsyncThunk(typePrefix, payloadCreator) {
  // 生成三个 action type
  const pending   = `${typePrefix}/pending`
  const fulfilled = `${typePrefix}/fulfilled`
  const rejected  = `${typePrefix}/rejected`

  // 返回一个 thunk action creator
  function actionCreator(arg) {
    // 这个函数会被 redux-thunk 中间件拦截并执行
    return async function(dispatch, getState) {
      dispatch({ type: pending })
      try {
        const result = await payloadCreator(arg, { dispatch, getState, rejectWithValue })
        dispatch({ type: fulfilled, payload: result })
      } catch (error) {
        dispatch({ type: rejected, error: error.message })
      }
    }
  }

  actionCreator.pending   = pending
  actionCreator.fulfilled = fulfilled
  actionCreator.rejected  = rejected

  return actionCreator
}

【代码注释】createAsyncThunk 返回的不是普通 action 对象,而是一个函数(thunk)。当被 dispatch 时,redux-thunk 中间件检测到这是一个函数,就执行它并传入 dispatchgetState。在异步操作前自动 dispatch pending,成功后 dispatch fulfilled,失败后 dispatch rejected,这样就把异步操作的状态管理完全标准化了。

5.3 完整异步 Slice 示例

// store/slices/productSlice.ts
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'

export interface IProduct {
  id: number
  name: string
  price: number
  image: string
  stock: number
}

interface ProductState {
  items: IProduct[]
  // 四态:初始/加载中/成功/失败
  status: 'idle' | 'loading' | 'succeeded' | 'failed'
  error: string | null
}

// 创建异步 thunk:第一个泛型是 fulfilled payload 类型,第二个是参数类型
export const fetchProducts = createAsyncThunk<IProduct[], void>(
  'products/fetchProducts',
  async (_, { rejectWithValue }) => {
    try {
      const response = await fetch('/api/products')
      if (!response.ok) throw new Error('服务器错误')
      return (await response.json()) as IProduct[]
    } catch (error) {
      // rejectWithValue 将自定义错误信息放入 action.payload(而非 action.error)
      return rejectWithValue((error as Error).message)
    }
  }
)

const productSlice = createSlice({
  name: 'products',
  initialState: {
    items: [],
    status: 'idle',
    error: null
  } as ProductState,
  reducers: {
    // 同步 action:清空商品列表
    clearProducts(state) {
      state.items = []
      state.status = 'idle'
    }
  },
  // extraReducers 处理 createAsyncThunk 生成的 action
  extraReducers: builder => {
    builder
      .addCase(fetchProducts.pending, state => {
        state.status = 'loading'
        state.error = null
      })
      .addCase(fetchProducts.fulfilled, (state, action) => {
        state.status = 'succeeded'
        state.items = action.payload  // 直接赋值,Immer 处理不可变
      })
      .addCase(fetchProducts.rejected, (state, action) => {
        state.status = 'failed'
        // rejectWithValue 的值在 action.payload 中
        state.error = (action.payload as string) ?? action.error.message ?? '未知错误'
      })
  }
})

export const { clearProducts } = productSlice.actions
export default productSlice.reducer

【代码注释】extraReducers 使用 builder 模式(链式调用 addCase)处理来自外部的 action,而不是 createSlice 内部 reducers 生成的 action。关键点:fetchProducts.pendingfetchProducts.fulfilledfetchProducts.rejected 是 createAsyncThunk 自动挂载到 action creator 上的子 action creator,用于在 addCase 中精确匹配对应状态。市面应用:电商商品列表页、管理后台数据表格的数据加载均使用此模式。

在组件中使用异步 thunk:

// components/ProductList.tsx(概念示例)
import { useEffect } from 'react'
import { useAppDispatch, useAppSelector } from '../store/hooks'
import { fetchProducts } from '../store/slices/productSlice'

function ProductList() {
  const dispatch = useAppDispatch()
  const { items, status, error } = useAppSelector(state => state.products)

  useEffect(() => {
    // status 为 idle 时才发起请求,避免重复请求
    if (status === 'idle') {
      dispatch(fetchProducts())
    }
  }, [status, dispatch])

  if (status === 'loading') return <div>加载中...</div>
  if (status === 'failed') return <div>加载失败:{error}</div>

  return (
    <div className="product-list">
      {items.map(product => (
        <div key={product.id}>{product.name} - ¥{product.price}</div>
      ))}
    </div>
  )
}

【代码注释】使用 status === 'idle' 作为触发条件而非每次 mount 都请求,防止路由切换时重复发起请求。这是 RTK 官方推荐的异步数据加载模式。error 字段来自 rejectWithValue 的返回值,可以直接展示给用户。市面应用:这个模式在后台管理系统(列表页+详情页+编辑页)中极为普遍。

【实战要点】

  • 经典应用场景:所有需要网络请求的数据加载(商品列表、用户信息、订单详情),以及需要乐观更新的场景(先更新 UI,请求失败再回滚)。
  • 常见坑:在 createAsyncThunk 的回调中直接 throw error 而不是 return rejectWithValue(msg)——两者的区别在于:直接 throw 时错误信息在 action.error.message(序列化受限),用 rejectWithValue 时自定义信息在 action.payload(推荐,可携带任意序列化数据)。
  • 最佳实践:用 'idle' | 'loading' | 'succeeded' | 'failed' 四态而非简单的 isLoading: boolean,这样可以区分"从未请求"和"请求中"两种状态,避免初始渲染时的加载闪烁。

【本章小结】

阶段Action Typestate.statusUI 展示
dispatch 前-'idle'
请求中xxx/pending'loading'加载动画
请求成功xxx/fulfilled'succeeded'数据列表
请求失败xxx/rejected'failed'错误提示

【面试考点】 Q:createAsyncThunk 中 rejectWithValue 和直接 throw error 有什么区别? A:两者都会触发 rejected action,但错误信息的位置不同。直接 throw 时,Thunk 会把错误序列化后放入 action.error,受 Redux 可序列化约束,Error 对象无法完整保留;用 rejectWithValue(value) 时,value 被放入 action.payload,可以是任意可序列化的数据(字符串、对象),更灵活,在 extraReducers 中也更易访问。推荐始终使用 rejectWithValue 以获得完整的自定义错误信息。

Q:为什么需要 extraReducers,在 createSlice 的 reducers 中直接处理不行吗? A:reducers 只能定义与当前 slice 的 name 挂钩的 action(即自动生成的 action)。createAsyncThunk 生成的 action type 格式是 typePrefix/pending 等,不属于任何 slice 自动管理的范围,必须用 extraReducers 引用外部 action。这也体现了 Redux 的可组合性——一个 slice 可以响应来自其他 slice 或 thunk 的 action,实现跨 slice 的联动。


六、createSelector 记忆化选择器

6.1 名词解释

  • Selector(选择器):从 Redux state 中提取数据的函数,形如 state => state.cart.items
  • 派生数据(Derived Data):从原始 state 计算出的数据,如购物车总价(由 items 计算得出)。
  • 记忆化(Memoization):缓存函数的计算结果,当输入不变时直接返回缓存,避免重复计算。
  • Reselect:提供记忆化 selector 的库,RTK 已将其集成为 createSelector
  • 浅比较(Shallow Equality):只比较对象的引用(===),不深入比较对象内部属性。

6.2 记忆化原理与性能收益

在这里插入图片描述

为什么需要记忆化?

useSelector 在每次 Redux state 变化时都会执行 selector 函数。即使 cart.items 没有变化,只要其他 slice(如 themeuser)的状态更新,所有组件的 selector 都会重新执行:

// 无记忆化的 selector:每次 state 变化都重新计算
const total = useAppSelector(state =>
  state.cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
// 问题:切换主题色时,这个 reduce 也会执行,尽管 cart.items 没有变化

createSelector 的记忆化策略:只有当输入 selector 的返回值发生变化(引用不等)时,才重新执行计算函数:

import { createSelector } from '@reduxjs/toolkit'
import type { RootState } from '../store'

// 输入 selector(原始数据提取)
const selectCartItems = (state: RootState) => state.cart.items

// 记忆化 selector(派生数据计算)
export const selectCartTotal = createSelector(
  selectCartItems,                        // 输入 selector
  items =>                               // 计算函数(只在 items 引用变化时执行)
    items.reduce((sum, item) => sum + item.price * item.quantity, 0)
)

export const selectCartCount = createSelector(
  selectCartItems,
  items => items.reduce((sum, item) => sum + item.quantity, 0)
)

// 工厂模式:参数化 selector
export const makeSelectIsInCart = (productId: number) =>
  createSelector(
    selectCartItems,
    items => items.some(i => i.id === productId)
  )

【代码注释】createSelector 接收 N 个输入 selector 和一个计算函数。内部用浅比较(===)缓存上一次的输入结果,只有当某个输入 selector 返回新引用时才重新计算。因为 Immer 的结构共享,未修改的子树引用不变,这两个机制配合完美。工厂模式(makeSelectIsInCart)解决了带参数的记忆化 selector 问题,在组件中:const isInCart = useAppSelector(makeSelectIsInCart(product.id))。市面应用:电商商品列表中每个商品卡片判断"是否已加入购物车",就是典型的参数化 selector 场景。

flowchart TD
    A[state 发生任何变化] --> B{selectCartItems\n返回值是否变化?}
    B -- 否(引用相同) --> C[返回缓存的 total\n不执行 reduce]
    B -- 是(新数组引用) --> D[执行 items.reduce...\n计算新 total]
    D --> E[缓存新结果]
    C --> F[组件不重渲染]
    E --> G{total 值是否变化?}
    G -- 否 --> F
    G -- 是 --> H[组件重渲染]

【代码注释】此图展示 createSelector 的两级缓存检查:第一级检查输入 selector 的返回值是否变化(引用比较),第二级检查计算结果是否变化(值比较)。只有通过两级检查才触发组件重渲染,大幅减少不必要的计算和渲染。

【实战要点】

  • 经典应用场景:购物车总价(price * quantity 累加)、列表过滤结果(基于搜索词过滤商品)、权限计算(基于用户角色派生可见菜单)——凡是"从原始数据计算出的派生数据"都应使用 createSelector。
  • 常见坑:在组件内部创建工厂 selector(const selector = makeSelectIsInCart(id)),每次渲染都创建新的 selector 实例,导致记忆化失效。正确做法是在组件外部或用 useMemo 创建 selector 实例。
  • 最佳实践createSelector 默认只缓存最后一次计算结果(缓存大小为 1)。若同一个 selector 在多个组件中以不同参数使用,需要用 createSelectorCreator 自定义缓存策略,或为每个组件实例创建独立 selector。

【本章小结】

场景普通 selectorcreateSelector
简单数据提取✅ 足够过度设计
派生数据计算(reduce/filter)⚠️ 每次 state 变化都重算✅ 输入不变不重算
带参数的 selector❌ 无法记忆化✅ 工厂模式
多组件共享同一 selector✅ 共享缓存

【面试考点】 Q:createSelector 的记忆化是如何实现的?它的缓存失效条件是什么? A:createSelector 内部维护上一次的输入列表和计算结果。每次调用时,对每个输入 selector 的返回值进行浅比较(===)。若所有输入都与上一次相同(引用相等),直接返回缓存的计算结果,不执行计算函数;若有任何一个输入引用变化,重新执行计算函数并缓存新结果。缓存失效的条件是「任意输入 selector 返回的值引用变化」。默认缓存大小为 1(只记住最后一次),这在同一 selector 以不同参数被多个组件同时使用时会造成频繁缓存失效,需要用工厂模式解决。


七、configureStore 与 TypeScript 类型集成

7.1 名词解释

  • configureStore:RTK 的 store 创建函数,自动集成 DevTools 和 redux-thunk 中间件。
  • RootState:整个应用 state 树的 TypeScript 类型,由 ReturnType<typeof store.getState> 自动推断。
  • AppDispatch:store.dispatch 函数的 TypeScript 类型,支持 thunk 函数的类型推断。
  • TypedUseSelectorHook:react-redux 提供的泛型类型,用于创建类型化的 useSelector hook。
  • 中间件(Middleware):Redux 的扩展机制,可以拦截 dispatch,用于异步处理、日志等。

7.2 Store 配置详解

// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'
import cartReducer from './slices/cartSlice'
import productReducer from './slices/productSlice'
import userReducer from './slices/userSlice'

export const store = configureStore({
  reducer: {
    cart: cartReducer,
    products: productReducer,
    user: userReducer
  },
  // middleware 配置(getDefaultMiddleware 已包含 redux-thunk)
  middleware: getDefaultMiddleware =>
    getDefaultMiddleware({
      // 状态或 action 中有不可序列化值(如 Date、函数)时需要关闭此检查
      serializableCheck: {
        // 忽略 redux-persist 的 action
        ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE']
      }
    })
  // devTools 在生产环境自动禁用,开发环境自动启用(可手动配置)
  // devTools: process.env.NODE_ENV !== 'production'
})

// 关键:从 store 推断类型,而非手动维护
export type RootState = ReturnType<typeof store.getState>
// AppDispatch 包含 thunk 类型支持
export type AppDispatch = typeof store.dispatch

【代码注释】configureStorereducer 接受一个对象,每个键成为 state 树的一个命名空间(state.cartstate.products)。ReturnType<typeof store.getState> 是 TypeScript 的工具类型,从函数的返回值类型自动推断,这样每次新增 slice 后,RootState 会自动包含新的字段,无需手动更新类型定义。市面应用:中大型 React 项目(10+ slice)全部采用此模式自动维护 RootState 类型。

7.3 类型化 Hooks 封装

// src/store/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './index'

// 类型化 dispatch:支持 thunk action 的类型推断
export const useAppDispatch = () => useDispatch<AppDispatch>()

// 类型化 selector:自动获得 state 的完整类型提示
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

【代码注释】这两行代码是 RTK + TypeScript 项目的标配。封装后,业务组件中的 useAppDispatchuseAppSelector 不需要每次都手动传泛型,IDE 可以自动推断 state 的所有字段和 selector 的返回类型。TypedUseSelectorHook<RootState> 是 react-redux 提供的工具类型,专门用于创建类型化的 useSelector。市面应用:所有使用 RTK + TypeScript 的项目都会有这个文件,通常位于 src/store/hooks.ts

在组件中使用的完整类型推断效果:

// 业务组件中(有完整 IDE 提示)
import { useAppDispatch, useAppSelector } from '../store/hooks'
import { addToCart, selectCartTotal } from '../store/slices/cartSlice'

function Component() {
  const dispatch = useAppDispatch()
  // total 自动推断为 number 类型
  const total = useAppSelector(selectCartTotal)
  // items 自动推断为 ICartItem[] 类型
  const items = useAppSelector(state => state.cart.items)

  // dispatch 知道 addToCart 需要 IProduct 类型参数
  const handleAdd = (product: IProduct) => dispatch(addToCart(product))
}

【实战要点】

  • 经典应用场景:TypeScript 项目的 store 配置是一次性工作,配置好后所有业务组件都享受完整类型推断,大幅减少运行时错误。
  • 常见坑:在 configureStorereducer 参数里直接传入合并后的 rootReducer 对象,而不是让 RTK 自己合并——这会导致 TypeScript 无法正确推断各 slice 的命名空间类型。
  • 最佳实践:将 storeRootStateAppDispatch 的导出放在同一个文件(store/index.ts),将 useAppDispatchuseAppSelector 放在 store/hooks.ts,业务组件只从 hooks.ts 导入,避免循环依赖。

【本章小结】

配置项作用关键点
reducer组合所有 slice reducer对象 key 成为 state 命名空间
middleware扩展 dispatch 能力默认包含 redux-thunk 和序列化检查
RootState全局 state 类型ReturnType 自动推断,无需手动维护
AppDispatchdispatch 函数类型包含 thunk 支持
useAppDispatch类型化 dispatch hook省去每次手动传泛型
useAppSelector类型化 selector hook自动补全 state 字段

【面试考点】 Q:为什么要封装 useAppDispatch 和 useAppSelector,直接用 useDispatch 和 useSelector 不行吗? A:可以用,但需要每次手动传泛型:useSelector<RootState, number>(state => state.cart.total)useDispatch<AppDispatch>()。封装后的 hook 已内置了应用特定的 RootStateAppDispatch 类型,使用时无需重复传泛型,且如果 store 类型发生变化(新增 slice),所有使用封装 hook 的组件都自动获得更新的类型,不会有遗漏。这是 RTK 官方文档明确推荐的模式。


八、完整购物车实战

8.1 购物车 Slice 设计

// src/store/slices/cartSlice.ts
import { createSlice, createSelector, PayloadAction } from '@reduxjs/toolkit'
import type { RootState } from '../index'

// 商品接口
export interface IProduct {
  id: number
  name: string
  price: number
  image: string
  stock: number
}

// 购物车商品(商品 + 数量)
export interface ICartItem extends IProduct {
  quantity: number
}

// 购物车 state 结构
interface CartState {
  items: ICartItem[]
}

const cartSlice = createSlice({
  name: 'cart',
  initialState: { items: [] } as CartState,
  reducers: {
    // 添加到购物车(已存在则增加数量,考虑库存上限)
    addToCart(state, action: PayloadAction<IProduct>) {
      const existing = state.items.find(i => i.id === action.payload.id)
      if (existing) {
        // 不超过库存上限
        existing.quantity = Math.min(existing.quantity + 1, action.payload.stock)
      } else {
        state.items.push({ ...action.payload, quantity: 1 })
      }
    },

    // 从购物车移除
    removeFromCart(state, action: PayloadAction<number>) {
      state.items = state.items.filter(i => i.id !== action.payload)
    },

    // 设置商品数量(数量 ≤ 0 时自动移除)
    setQuantity(state, action: PayloadAction<{ id: number; quantity: number }>) {
      const { id, quantity } = action.payload
      const item = state.items.find(i => i.id === id)
      if (!item) return
      if (quantity <= 0) {
        state.items = state.items.filter(i => i.id !== id)
      } else {
        item.quantity = Math.min(quantity, item.stock)
      }
    },

    // 清空购物车
    clearCart(state) {
      state.items = []
    }
  }
})

// ── Selectors ──────────────────────────────────────────────────

// 基础 selector
export const selectCartItems = (state: RootState) => state.cart.items

// 记忆化:总价
export const selectCartTotal = createSelector(
  selectCartItems,
  items => items.reduce((sum, item) => sum + item.price * item.quantity, 0)
)

// 记忆化:总件数
export const selectCartCount = createSelector(
  selectCartItems,
  items => items.reduce((sum, item) => sum + item.quantity, 0)
)

// 记忆化工厂:判断某商品是否在购物车中
export const makeSelectIsInCart = (productId: number) =>
  createSelector(selectCartItems, items => items.some(i => i.id === productId))

// 记忆化工厂:获取购物车中某商品的数量
export const makeSelectItemQuantity = (productId: number) =>
  createSelector(
    selectCartItems,
    items => items.find(i => i.id === productId)?.quantity ?? 0
  )

export const { addToCart, removeFromCart, setQuantity, clearCart } = cartSlice.actions
export default cartSlice.reducer

【代码注释】这个 cartSlice 包含了购物车的完整业务逻辑:(1)addToCart 处理"已在购物车中增量"和"新增"两种情况,并用 Math.min 限制库存上限;(2)setQuantity 在数量降至 0 时自动移除,避免出现"0件商品"的异常状态;(3)工厂 selector(makeSelectIsInCartmakeSelectItemQuantity)为每个商品卡片提供独立的记忆化 selector。市面应用:这是标准的电商购物车 Redux 实现,京东、淘宝等平台的前端购物车逻辑与此高度相似。

8.2 ProductCard 组件

// src/components/ProductCard.tsx
import { memo } from 'react'
import { useAppDispatch, useAppSelector } from '../store/hooks'
import {
  addToCart,
  setQuantity,
  makeSelectIsInCart,
  makeSelectItemQuantity
} from '../store/slices/cartSlice'
import type { IProduct } from '../store/slices/cartSlice'

interface Props {
  product: IProduct
}

// memo 避免父组件重渲染时不必要的子组件重渲染
const ProductCard = memo(function ProductCard({ product }: Props) {
  const dispatch = useAppDispatch()

  // 工厂 selector:每个 ProductCard 有独立的记忆化 selector 实例
  const isInCart = useAppSelector(makeSelectIsInCart(product.id))
  const quantity = useAppSelector(makeSelectItemQuantity(product.id))

  const handleDecrease = () => {
    dispatch(setQuantity({ id: product.id, quantity: quantity - 1 }))
  }

  const handleIncrease = () => {
    dispatch(setQuantity({ id: product.id, quantity: quantity + 1 }))
  }

  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <div className="product-info">
        <h3>{product.name}</h3>
        <p className="price">¥{product.price.toFixed(2)}</p>
        <p className="stock">库存:{product.stock} 件</p>
      </div>

      {isInCart ? (
        <div className="quantity-control">
          <button onClick={handleDecrease} disabled={quantity <= 1}>-</button>
          <span>{quantity}</span>
          <button onClick={handleIncrease} disabled={quantity >= product.stock}>+</button>
          <button className="remove-btn" onClick={() =>
            dispatch({ type: 'cart/removeFromCart', payload: product.id })
          }>
            移除
          </button>
        </div>
      ) : (
        <button
          className="add-btn"
          onClick={() => dispatch(addToCart(product))}
          disabled={product.stock === 0}
        >
          {product.stock === 0 ? '已售罄' : '加入购物车'}
        </button>
      )}
    </div>
  )
})

export default ProductCard

【代码注释】几个设计要点:(1)用 React.memo 包裹组件,配合 Redux 的结构共享,确保只有与该商品相关的 selector 输出变化时组件才重渲染;(2)makeSelectIsInCart(product.id) 在每次渲染时都会调用工厂函数,为避免每次创建新实例导致记忆化失效,实际项目中应将其用 useMemo 稳定:const isInCartSelector = useMemo(() => makeSelectIsInCart(product.id), [product.id]);(3)disabled={quantity >= product.stock} 防止超过库存的 UI 层防护(reducer 也有防护,双重保险)。

8.3 CartSidebar 组件

// src/components/CartSidebar.tsx
import { useAppDispatch, useAppSelector } from '../store/hooks'
import {
  selectCartItems,
  selectCartTotal,
  selectCartCount,
  removeFromCart,
  clearCart
} from '../store/slices/cartSlice'

function CartSidebar() {
  const dispatch = useAppDispatch()
  const items = useAppSelector(selectCartItems)
  const total = useAppSelector(selectCartTotal)   // 记忆化,不重复计算
  const count = useAppSelector(selectCartCount)   // 记忆化,不重复计算

  if (items.length === 0) {
    return (
      <aside className="cart-sidebar cart-empty">
        <p>购物车空空如也,快去挑选商品吧~</p>
      </aside>
    )
  }

  return (
    <aside className="cart-sidebar">
      <header>
        <h2>购物车</h2>
        <span className="cart-count">共 {count} 件</span>
      </header>

      <ul className="cart-items">
        {items.map(item => (
          <li key={item.id} className="cart-item">
            <img src={item.image} alt={item.name} width={60} />
            <div className="item-detail">
              <p className="item-name">{item.name}</p>
              <p className="item-price">
                ¥{item.price} × {item.quantity}
                = <strong>¥{(item.price * item.quantity).toFixed(2)}</strong>
              </p>
            </div>
            <button
              className="remove-btn"
              onClick={() => dispatch(removeFromCart(item.id))}
              aria-label={`移除 ${item.name}`}
            >
              ✕
            </button>
          </li>
        ))}
      </ul>

      <footer className="cart-footer">
        <p className="total">
          合计:<strong>¥{total.toFixed(2)}</strong>
        </p>
        <div className="cart-actions">
          <button className="clear-btn" onClick={() => dispatch(clearCart())}>
            清空购物车
          </button>
          <button className="checkout-btn">
            去结算({count} 件)
          </button>
        </div>
      </footer>
    </aside>
  )
}

export default CartSidebar

【代码注释】selectCartTotalselectCartCount 是记忆化 selector,只有 items 数组引用变化时才重新计算,避免了每次无关 state 更新时的重复 reduce 计算。aria-label 属性提升了无障碍访问体验(屏幕阅读器会读出"移除商品名")。市面应用:这是标准的电商购物车侧边栏实现,与 Amazon、京东购物车的 React 实现结构高度相似。

【实战要点】

  • 经典应用场景:购物车页面、订单确认页、结算页——这类页面同时需要读取多种派生数据(总价、总件数、优惠后价格),是 createSelector 的经典用武之地。
  • 常见坑:在 CartSidebar 中直接用 items.length 显示数量而非 selectCartCount——如果每件商品有 quantity 字段,items.length 是商品种类数而非商品总件数,这是业务逻辑 bug。
  • 最佳实践:购物车相关的所有派生计算(总价、折扣、运费、总件数)都应放在 selector 中,而不是在组件的 render 函数里计算,这样逻辑集中、可复用、可测试。

【本章小结】

组件读取的 selectordispatch 的 action
ProductCardisInCart, quantityaddToCart, setQuantity
CartSidebaritems, total, countremoveFromCart, clearCart
Header/NavBarselectCartCount-

【面试考点】 Q:如何用 TypeScript 为 createSlice 的 PayloadAction 提供精确类型? A:在 reducers 的函数参数中用 PayloadAction<T> 泛型标注 action 的类型,T 是 payload 的类型。例如 addToCart(state, action: PayloadAction<IProduct>) 告诉 TypeScript action.payload 是 IProduct 类型,编译时就能检查 dispatch(addToCart(xxx)) 时 xxx 是否符合 IProduct 接口。RTK 会从这个类型推断出 action creator 的参数类型,实现端到端的类型安全,不需要在 action creator 调用处重复标注类型。


九、本地持久化与 Redux Persist

购物车数据在页面刷新后应当保留。redux-persist 将 Redux state 序列化存入 localStorage,并在应用启动时自动恢复。

// src/store/index.ts(集成 redux-persist)
import { configureStore, combineReducers } from '@reduxjs/toolkit'
import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER } from 'redux-persist'
import storage from 'redux-persist/lib/storage'  // 默认使用 localStorage
import cartReducer from './slices/cartSlice'
import productReducer from './slices/productSlice'

// 购物车持久化配置
const cartPersistConfig = {
  key: 'cart',             // localStorage 中的 key
  storage,                 // 使用 localStorage
  whitelist: ['items']     // 只持久化 items,不持久化其他字段
}

const rootReducer = combineReducers({
  cart: persistReducer(cartPersistConfig, cartReducer),
  products: productReducer   // 商品列表不需要持久化(每次从接口拉取)
})

export const store = configureStore({
  reducer: rootReducer,
  middleware: getDefaultMiddleware =>
    getDefaultMiddleware({
      serializableCheck: {
        // 忽略 redux-persist 内部使用的非序列化 action
        ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
      }
    })
})

export const persistor = persistStore(store)
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

【代码注释】whitelist 控制只持久化 items 字段——如果 state 中还有 status('loading'/'succeeded')等字段,不应该持久化,否则刷新后 status 仍是 'loading' 会导致 UI 异常。ignoredActions 必须包含 redux-persist 的内部 action,否则 RTK 的序列化检查中间件会报警告。市面应用:电商平台购物车、表单草稿自动保存、用户偏好设置均使用此方案。

// src/App.tsx(PersistGate 包裹)
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react'
import { store, persistor } from './store'

function App() {
  return (
    <Provider store={store}>
      {/* PersistGate 等待 localStorage 恢复完成后再渲染子组件 */}
      <PersistGate loading={<div>状态恢复中...</div>} persistor={persistor}>
        <RouterProvider router={router} />
      </PersistGate>
    </Provider>
  )
}

【代码注释】PersistGate 是一个等待门,在 redux-persist 从 localStorage 读取并恢复 state 之前显示 loading 内容,恢复完成后才渲染子组件。这避免了"state 还没从 localStorage 恢复时购物车显示为空"的闪烁问题。市面应用:所有需要用户刷新后保留状态的 Web 应用(购物车、编辑器草稿、用户设置)都需要类似的"恢复门"机制。

【实战要点】

  • 经典应用场景:购物车商品列表、用户偏好(主题/语言)、表单中途保存的草稿——凡是"用户操作后刷新不应丢失"的数据都应持久化。
  • 常见坑:持久化了 status: 'loading' 字段,导致刷新后应用一直显示加载状态。解决:用 whitelist 只持久化需要保留的字段,或在 REHYDRATE action 的 case 中将 status 重置为 'idle'
  • 最佳实践:不要持久化服务器数据缓存(如商品列表、用户详情),这些数据应在每次启动时重新请求,以确保数据新鲜度;只持久化用户主动产生的数据(购物车选择、偏好设置)。

【本章小结】

配置项作用注意事项
keylocalStorage 的存储 key不同 slice 用不同 key
storage存储引擎默认 localStorage,可换 sessionStorage
whitelist只持久化指定字段排除 status、error 等动态字段
PersistGate恢复完成前的等待门loading prop 显示恢复中的 UI
ignoredActions忽略内部 action 的序列化检查必须配置,否则控制台报警告

十、交互式购物车 HTML 演示

以下是一个完整可运行的购物车演示,用纯 HTML + JavaScript 模拟 Redux 的状态管理逻辑,可以直接保存为 .html 文件在浏览器中打开:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Redux 购物车原理演示</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; background: #f5f5f5; color: #333; }

    .app { display: grid; grid-template-columns: 1fr 380px; gap: 0; min-height: 100vh; }

    /* 左侧商品区 */
    .main { padding: 24px; }
    .main h1 { font-size: 22px; margin-bottom: 20px; color: #1a1a1a; }

    .product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; }

    .product-card {
      background: #fff;
      border-radius: 12px;
      overflow: hidden;
      box-shadow: 0 2px 8px rgba(0,0,0,.08);
      transition: transform .2s, box-shadow .2s;
    }
    .product-card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(0,0,0,.12); }

    .product-img {
      width: 100%; height: 160px; object-fit: cover;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      display: flex; align-items: center; justify-content: center;
      font-size: 48px;
    }

    .product-body { padding: 16px; }
    .product-name { font-size: 15px; font-weight: 600; margin-bottom: 6px; }
    .product-price { color: #e53e3e; font-size: 18px; font-weight: 700; margin-bottom: 4px; }
    .product-stock { color: #999; font-size: 12px; margin-bottom: 12px; }

    .btn { border: none; border-radius: 8px; cursor: pointer; font-size: 14px; padding: 8px 16px; transition: all .2s; }
    .btn-add { background: #4f6ef7; color: #fff; width: 100%; }
    .btn-add:hover { background: #3b5bdb; }
    .btn-add:disabled { background: #ccc; cursor: not-allowed; }
    .btn-add.in-cart { background: #38a169; }

    .qty-control { display: flex; align-items: center; gap: 8px; }
    .qty-btn { width: 32px; height: 32px; border-radius: 8px; background: #f0f0f0; border: none; cursor: pointer; font-size: 18px; font-weight: 700; }
    .qty-btn:hover:not(:disabled) { background: #ddd; }
    .qty-btn:disabled { opacity: .4; cursor: not-allowed; }
    .qty-num { flex: 1; text-align: center; font-size: 16px; font-weight: 600; }

    /* 右侧购物车 */
    .cart-sidebar {
      background: #fff;
      border-left: 1px solid #eee;
      display: flex; flex-direction: column;
      height: 100vh; position: sticky; top: 0;
    }

    .cart-header {
      padding: 20px 20px 16px;
      border-bottom: 1px solid #f0f0f0;
      display: flex; justify-content: space-between; align-items: center;
    }
    .cart-header h2 { font-size: 18px; }
    .cart-badge {
      background: #e53e3e; color: #fff;
      border-radius: 12px; padding: 2px 8px; font-size: 12px;
    }

    .cart-items { flex: 1; overflow-y: auto; padding: 12px 20px; }

    .cart-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #999; gap: 8px; }
    .cart-empty .empty-icon { font-size: 48px; }

    .cart-item {
      display: flex; align-items: center; gap: 12px;
      padding: 12px 0; border-bottom: 1px solid #f5f5f5;
    }
    .cart-item-icon { font-size: 32px; }
    .cart-item-info { flex: 1; }
    .cart-item-name { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
    .cart-item-price { font-size: 12px; color: #999; }
    .cart-item-subtotal { font-size: 14px; font-weight: 600; color: #e53e3e; white-space: nowrap; }
    .btn-remove { background: none; border: none; cursor: pointer; color: #bbb; font-size: 16px; padding: 4px; }
    .btn-remove:hover { color: #e53e3e; }

    .cart-footer {
      padding: 16px 20px 24px;
      border-top: 1px solid #f0f0f0;
    }
    .cart-total { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
    .total-label { font-size: 14px; color: #666; }
    .total-amount { font-size: 22px; font-weight: 700; color: #e53e3e; }

    .btn-checkout { width: 100%; background: #e53e3e; color: #fff; padding: 12px; border-radius: 10px; font-size: 15px; font-weight: 600; }
    .btn-checkout:hover { background: #c53030; }
    .btn-clear { width: 100%; background: #f5f5f5; color: #666; padding: 10px; border-radius: 10px; font-size: 14px; margin-bottom: 8px; }
    .btn-clear:hover { background: #eee; }

    /* 状态面板 */
    .state-panel {
      margin-top: 32px; background: #1a1a2e; border-radius: 12px;
      padding: 16px; color: #a8ff78;
    }
    .state-panel h3 { color: #fff; font-size: 13px; margin-bottom: 10px; font-family: monospace; }
    .state-code { font-family: 'Courier New', monospace; font-size: 12px; line-height: 1.6; white-space: pre-wrap; max-height: 200px; overflow-y: auto; }

    /* 操作日志 */
    .action-log {
      margin-top: 16px; background: #1a1a2e; border-radius: 12px;
      padding: 16px;
    }
    .action-log h3 { color: #fff; font-size: 13px; margin-bottom: 10px; font-family: monospace; }
    .log-list { max-height: 160px; overflow-y: auto; }
    .log-item { font-family: monospace; font-size: 11px; padding: 3px 0; border-bottom: 1px solid #2a2a4a; }
    .log-item .log-type { color: #ffd700; }
    .log-item .log-payload { color: #87ceeb; }
    .log-item .log-time { color: #666; float: right; }

    @media (max-width: 768px) {
      .app { grid-template-columns: 1fr; }
      .cart-sidebar { height: auto; position: static; }
    }
  </style>
</head>
<body>
<div class="app">
  <!-- 左侧:商品列表 -->
  <div class="main">
    <h1>商品列表</h1>
    <div class="product-grid" id="productGrid"></div>

    <!-- Redux State 可视化面板 -->
    <div class="state-panel">
      <h3>// Redux State 实时快照(模拟 DevTools)</h3>
      <div class="state-code" id="stateDisplay"></div>
    </div>

    <!-- Action 日志 -->
    <div class="action-log">
      <h3>// Action 日志(模拟 Redux DevTools)</h3>
      <div class="log-list" id="actionLog"></div>
    </div>
  </div>

  <!-- 右侧:购物车 -->
  <div class="cart-sidebar">
    <div class="cart-header">
      <h2>购物车</h2>
      <span class="cart-badge" id="cartBadge">0</span>
    </div>
    <div id="cartContent" class="cart-items">
      <div class="cart-empty">
        <div class="empty-icon">🛒</div>
        <p>购物车空空如也</p>
      </div>
    </div>
    <div class="cart-footer" id="cartFooter" style="display:none">
      <div class="cart-total">
        <span class="total-label">合计</span>
        <span class="total-amount" id="cartTotal">¥0.00</span>
      </div>
      <button class="btn btn-clear" onclick="store.dispatch({ type: 'cart/clearCart' })">清空购物车</button>
      <button class="btn btn-checkout" id="checkoutBtn">去结算</button>
    </div>
  </div>
</div>

<script>
// ══════════════════════════════════════════════════
// 模拟 Redux Store 实现
// ══════════════════════════════════════════════════

// 商品数据(模拟数据库)
const PRODUCTS = [
  { id: 1, name: '无线蓝牙耳机', price: 299, stock: 5, icon: '🎧' },
  { id: 2, name: '机械键盘',     price: 599, stock: 3, icon: '⌨️' },
  { id: 3, name: '便携充电宝',   price: 149, stock: 10, icon: '🔋' },
  { id: 4, name: '智能手环',     price: 199, stock: 8, icon: '⌚' },
  { id: 5, name: '降噪鼠标',     price: 399, stock: 2, icon: '🖱️' },
  { id: 6, name: '4K 摄像头',   price: 699, stock: 4, icon: '📷' },
]

// ── 纯函数 Reducer(模拟 cartSlice 的 reducer) ──
function cartReducer(state = { items: [] }, action) {
  // 注意:这里我们"手动实现"不可变更新(生产中由 Immer 处理)
  switch (action.type) {
    case 'cart/addToCart': {
      const product = action.payload
      const existingIdx = state.items.findIndex(i => i.id === product.id)
      if (existingIdx >= 0) {
        const items = state.items.map((item, idx) =>
          idx === existingIdx
            ? { ...item, quantity: Math.min(item.quantity + 1, product.stock) }
            : item
        )
        return { ...state, items }
      }
      return { ...state, items: [...state.items, { ...product, quantity: 1 }] }
    }

    case 'cart/removeFromCart': {
      return { ...state, items: state.items.filter(i => i.id !== action.payload) }
    }

    case 'cart/setQuantity': {
      const { id, quantity } = action.payload
      if (quantity <= 0) {
        return { ...state, items: state.items.filter(i => i.id !== id) }
      }
      const product = PRODUCTS.find(p => p.id === id)
      const maxQty = product ? product.stock : 999
      return {
        ...state,
        items: state.items.map(item =>
          item.id === id
            ? { ...item, quantity: Math.min(quantity, maxQty) }
            : item
        )
      }
    }

    case 'cart/clearCart':
      return { ...state, items: [] }

    default:
      return state
  }
}

// ── 简易 Store 实现(模拟 Redux createStore 逻辑) ──
function createStore(reducer) {
  let state = reducer(undefined, { type: '@@INIT' })
  const listeners = []
  const actionHistory = []

  return {
    getState: () => state,

    dispatch(action) {
      // 1. 调用 reducer 生成新 state(纯函数)
      const prevState = state
      state = reducer(state, action)

      // 记录 action 历史(模拟 Redux DevTools)
      actionHistory.unshift({
        type: action.type,
        payload: action.payload,
        time: new Date().toLocaleTimeString()
      })
      if (actionHistory.length > 20) actionHistory.pop()

      // 2. 通知所有订阅者(类似 React 的 setState 触发重渲染)
      listeners.forEach(fn => fn(state, prevState, action))

      return action
    },

    subscribe(listener) {
      listeners.push(listener)
      return () => {
        const idx = listeners.indexOf(listener)
        if (idx > -1) listeners.splice(idx, 1)
      }
    },

    getActionHistory: () => actionHistory
  }
}

// ── Selectors(记忆化简化版) ──
const selectCartItems = state => state.items
const selectCartTotal = state =>
  state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
const selectCartCount = state =>
  state.items.reduce((sum, item) => sum + item.quantity, 0)
const selectIsInCart = (state, id) => state.items.some(i => i.id === id)
const selectItemQuantity = (state, id) =>
  state.items.find(i => i.id === id)?.quantity ?? 0

// ── 创建 store ──
const store = createStore(cartReducer)

// ══════════════════════════════════════════════════
// UI 渲染层(模拟 React 组件)
// ══════════════════════════════════════════════════

// 渲染商品列表
function renderProducts() {
  const cartState = store.getState()
  const grid = document.getElementById('productGrid')

  grid.innerHTML = PRODUCTS.map(product => {
    const inCart = selectIsInCart(cartState, product.id)
    const qty = selectItemQuantity(cartState, product.id)
    const soldOut = product.stock === 0

    return `
      <div class="product-card" data-id="${product.id}">
        <div class="product-img">${product.icon}</div>
        <div class="product-body">
          <div class="product-name">${product.name}</div>
          <div class="product-price">¥${product.price.toFixed(2)}</div>
          <div class="product-stock">库存:${product.stock} 件</div>
          ${inCart ? `
            <div class="qty-control">
              <button class="qty-btn" onclick="dispatch('cart/setQuantity', {id:${product.id}, quantity:${qty - 1}})"
                ${qty <= 1 ? 'disabled' : ''}>−</button>
              <span class="qty-num">${qty}</span>
              <button class="qty-btn" onclick="dispatch('cart/setQuantity', {id:${product.id}, quantity:${qty + 1}})"
                ${qty >= product.stock ? 'disabled' : ''}>+</button>
            </div>
          ` : `
            <button class="btn btn-add ${inCart ? 'in-cart' : ''}"
              onclick="dispatch('cart/addToCart', ${JSON.stringify(product)})"
              ${soldOut ? 'disabled' : ''}>
              ${soldOut ? '已售罄' : '加入购物车'}
            </button>
          `}
        </div>
      </div>
    `
  }).join('')
}

// 渲染购物车
function renderCart() {
  const cartState = store.getState()
  const items = selectCartItems(cartState)
  const total = selectCartTotal(cartState)
  const count = selectCartCount(cartState)

  // 更新角标
  document.getElementById('cartBadge').textContent = count
  document.getElementById('cartBadge').style.display = count > 0 ? '' : 'none'

  const cartContent = document.getElementById('cartContent')
  const cartFooter = document.getElementById('cartFooter')

  if (items.length === 0) {
    cartContent.innerHTML = `
      <div class="cart-empty">
        <div class="empty-icon">🛒</div>
        <p>购物车空空如也</p>
        <p style="font-size:12px;color:#ccc;margin-top:4px">快去挑选商品吧</p>
      </div>`
    cartFooter.style.display = 'none'
    return
  }

  cartContent.innerHTML = items.map(item => `
    <div class="cart-item">
      <div class="cart-item-icon">${item.icon}</div>
      <div class="cart-item-info">
        <div class="cart-item-name">${item.name}</div>
        <div class="cart-item-price">¥${item.price} × ${item.quantity}</div>
      </div>
      <div class="cart-item-subtotal">¥${(item.price * item.quantity).toFixed(2)}</div>
      <button class="btn-remove" onclick="dispatch('cart/removeFromCart', ${item.id})" title="移除">✕</button>
    </div>
  `).join('')

  document.getElementById('cartTotal').textContent = ${total.toFixed(2)}`
  cartFooter.style.display = 'block'

  // 结算按钮
  document.getElementById('checkoutBtn').onclick = () => {
    alert(`结算成功!\n共 ${count} 件商品\n合计:¥${total.toFixed(2)}`)
    store.dispatch({ type: 'cart/clearCart' })
  }
}

// 渲染 State 快照(模拟 Redux DevTools)
function renderStatePanel() {
  const state = store.getState()
  document.getElementById('stateDisplay').textContent =
    JSON.stringify({ cart: state }, null, 2)
}

// 渲染 Action 日志
function renderActionLog() {
  const history = store.getActionHistory()
  const logEl = document.getElementById('actionLog')
  logEl.innerHTML = history.map(entry => `
    <div class="log-item">
      <span class="log-type">${entry.type}</span>
      ${entry.payload !== undefined
        ? `<span class="log-payload"> → ${JSON.stringify(entry.payload)}</span>`
        : ''}
      <span class="log-time">${entry.time}</span>
    </div>
  `).join('')
}

// 统一 dispatch 函数(供 HTML onclick 调用)
function dispatch(type, payload) {
  store.dispatch({ type, payload })
}

// ── 订阅 store,state 变化时重新渲染 ──
store.subscribe(() => {
  renderProducts()
  renderCart()
  renderStatePanel()
  renderActionLog()
})

// ── 初始渲染 ──
renderProducts()
renderCart()
renderStatePanel()
</script>
</body>
</html>

【代码注释】这个演示从零实现了 Redux 的核心机制:(1)createStore 函数实现了 getStatedispatchsubscribe 三个核心 API,展示 Redux store 的本质;(2)cartReducer 手写不可变更新,让你看清 Immer 在生产代码中帮你省去的工作;(3)store.subscribe 订阅 state 变化并重新渲染 UI,这正是 react-redux 的 useSelector 的底层原理;(4)State 快照面板实时展示 Redux state,模拟 Redux DevTools 的核心功能;(5)Action 日志记录每次 dispatch 的操作历史,展示 Redux 可追踪性的实际意义。将此文件在浏览器中打开,即可体验完整的购物车操作并观察 Redux state 的实时变化。


总结

知识点思维导图

mindmap
  root((Redux Toolkit))
    核心原则
      单一数据源
      State只读
      纯函数Reducer
    核心API
      createSlice
        自动生成ActionType
        自动生成ActionCreator
        内置Immer
      configureStore
        自动集成DevTools
        内置redux-thunk
      createAsyncThunk
        pending状态
        fulfilled状态
        rejected状态
      createSelector
        记忆化
        结构共享配合
        工厂模式
    底层原理
      Immer
        ES6 Proxy拦截
        Draft草稿对象
        结构共享算法
      TypeScript集成
        RootState推断
        AppDispatch推断
        类型化Hooks
    工程实践
      Slice文件组织
      持久化redux-persist
      DevTools调试
      性能优化

【代码注释】此思维导图展示了 Redux Toolkit 的完整知识体系,从核心原则到底层原理再到工程实践,形成完整的学习路径。建议按"核心原则 → 核心 API → 底层原理 → 工程实践"的顺序掌握,每个节点都有对应的代码示例和面试考点。

高频面试题速查

#问题核心答案要点
1Redux 三大原则是什么?单一数据源、State 只读、纯函数 Reducer;解释每条原则的工程价值
2Immer 如何实现不可变更新?ES6 Proxy 代理 baseState 创建 draft,拦截写操作记录变更,recipe 完成后结构共享生成新对象
3createSlice 的 name 有什么作用?action type 的命名空间前缀,格式为 name/reducerKey,自动生成避免手写常量
4createAsyncThunk 的三个状态是什么?pending(请求中)、fulfilled(成功)、rejected(失败);在 extraReducers 中处理
5createSelector 的记忆化原理?缓存输入 selector 的上一次返回值,浅比较输入变化,输入不变直接返回缓存结果
6为什么封装 useAppDispatch 和 useAppSelector?内置 RootState 和 AppDispatch 类型,省去每次手动传泛型,IDE 自动补全更完整
7rejectWithValue 和 throw 有什么区别?rejectWithValue 的值在 action.payload,throw 的错误在 action.error,前者更灵活
8RTK 相比传统 Redux 解决了什么问题?样板代码、手写不可变更新、异步处理配置、DevTools 集成四大痛点

学习建议

掌握顺序

  1. 先理解 Redux 三大原则(理论基础)
  2. 手写一次传统 Redux(体验痛点,才能理解 RTK 的价值)
  3. 学习 createSlice 的使用,理解 Immer 的作用
  4. 学习 createAsyncThunk 处理网络请求
  5. 学习 createSelector 优化性能
  6. TypeScript 集成(RootState、AppDispatch、类型化 Hooks)
  7. 完整项目实战(购物车或用户管理)

调试工具:安装 Redux DevTools 浏览器扩展(Chrome/Firefox),配合 configureStore 自动集成,可以实时查看 state 快照、action 日志,并进行时间旅行调试。

进阶方向

  • RTK Query:RTK 官方的数据获取和缓存方案,可替代 createAsyncThunk + useEffect 的组合
  • Entity Adapter:RTK 内置的实体标准化工具,优化列表数据的 CRUD 操作
  • Immer 独立使用:Immer 可以脱离 RTK 单独使用,在任何需要不可变数据操作的场景中发挥价值

参考资料