React 文档
导读与全书目录表:见同目录 01-React 入门:简介·特性与设计范式.md 开头。
说说你对 redux 的理解
回答
redux 是前端项目公共状态管理库,特点是强约束、强规范、不宜出错可预测
# redux 四大核心概念
Store:应用唯一的状态仓库,内部维护完整的 state 树,提供dispatch(触发 action)、getState(获取最新状态)、subscribe(监听状态变化)三个核心方法。
State:只读的应用状态数据,所有变更必须通过返回新对象实现,不可直接修改原数据。
Action:普通 JS 对象,必须带type字段标识动作类型,可选payload字段携带附加数据,是改变状态的唯一 “信号”,只描述行为,不处理更新逻辑。
Reducer:状态更新的纯函数执行者,根据 action 的 type 匹配处理逻辑,通过不可变更新返回全新 state,不修改原 state、不产生任何副作用。
# redux 有三大核心设计原则
单一数据源:核心解决多数据源的状态同步难题,同时让全应用状态可集中管理、快照留存、回溯复现,Redux DevTools 的时间旅行、服务端渲染的状态注水、全局状态持久化,都完全基于这个原则实现。
State 只读,仅通过 Action 触发变更:核心解决状态变更碎片化的问题,把所有状态修改收敛到唯一的 dispatch 入口,杜绝了 UI 组件、业务逻辑中零散的 state 修改,让每一次状态变化都有迹可循,哪怕是线上复杂 bug,也能通过 Action 日志完整复现用户操作。
纯函数 Reducer 执行不可变更新:一方面,纯函数保证了状态更新的可预测性,无副作用就没有隐藏的状态变化,无需关注外部环境,极易做单元测试;另一方面,不可变更新既符合纯函数的入参不可修改要求,也完美适配 React 的引用对比更新机制,同时支撑了状态快照、历史回溯的核心能力。
# redux 核心工作流(单向数据流闭环)
整个流程固定单向,无任何捷径,这也是 Redux 可预测性的核心:
UI 组件通过dispatch方法,触发一个描述行为的 Action;
Store 接收到 Action 后,将当前旧 State 和 Action 一起传给 Reducer;
Reducer 匹配对应逻辑,完成不可变更新,返回全新的 State;
Store 接收到新 State 后,通知所有订阅了状态变化的组件;
组件拿到最新 State,触发 UI 重新渲染,完成整个闭环。
# 日常开发,结合 react-redux
Redux 本身和 React 无强绑定,日常开发通过官方react-redux库完成两者连接:
用Provider组件将 Store 注入应用根组件,让所有子组件可访问 Store;
函数组件时代,通过useSelector钩子提取组件所需的状态,通过useDispatch钩子获取 dispatch 方法触发 action,替代了类组件时代的connect高阶组件,写法更简洁,也更贴合 React Hooks 的开发习惯。
# 异步方案 - redux-thunk
redux-thunk,就是通过中间件让 dispatch 支持接收函数,在函数内执行异步操作,完成后再 dispatch 真正的 action
# 异步方案 - redux-saga
redux-saga则基于 Generator 函数,实现了复杂异步流程的精准控制,比如竞态处理、任务取消、重试、串行 / 并行流管控,适合超复杂的业务异步场景。
# 异步方案 - Redux Toolkit(RTK)
原生 Redux 存在模板代码冗余、手动不可变更新繁琐易出错、TypeScript 类型支持差、reducer 拆分成本高的痛点,而RTK 是官方当前强推的标准开发范式,也是现在企业级项目的绝对主流。
它内置了 immer 库支持 “mutable 写法” 的不可变更新,封装了createSlice、configureStore、createAsyncThunk等核心 API,默认集成 thunk 中间件和 DevTools 支持,一行代码完成 reducer 拆分和 store 配置,极大减少了模板代码,同时完美适配 TypeScript,从根本上解决了原生 Redux 的开发痛点。Redux 核心与使用场景
Redux 是基于 Flux 架构的状态管理库,核心是 “单一数据源、状态只读、纯函数修改”,由 Store(存储状态)、Action(描述状态变更)、Reducer(纯函数,根据 Action 更新状态)组成。它适合跨组件、多层级共享状态的场景,比如用户登录状态、全局主题配置。实际项目中,我会结合 Redux Toolkit(官方推荐)简化 Redux 代码,避免手写重复的 action 和 reducer,同时用 useSelector 和 useDispatch Hook 在组件中获取 / 修改状态,替代传统的 connect 高阶组件。状态管理
要点速览
# 主流方案对比
特性 Zustand Redux Context + useState
代码量 极少 多(action/reducer) 中(Provider 包裹)
学习成本 极低 高 中
重渲染 细粒度(精准订阅) 需手动优化 全量渲染(性能差)
异步处理 原生支持 需 redux-thunk/saga 原生支持
适用场景 中小型、快速开发 大型复杂项目 简单共享、低频更新Zustand 状态管理
Zustand 是 React 生态里轻量、无 Provider、极简 API 的状态管理库,主打 “用 Hook 管理全局状态”,替代 Context / Redux,兼顾开发效率与性能。
要点速览
# 特点
德语 “状态” 之意,1KB 左右、零依赖。
# 核心优势(对比 Context/Redux):
✅ 无 Provider:不用包裹根组件,直接 import store 使用。
✅ 极简 API:仅 create 一个核心函数,无 action/reducer 模板代码。
✅ 细粒度更新:组件只订阅需要的字段,避免 Context 全量渲染。
✅ TS 友好:天然支持类型推导,无需额外配置。
✅ 中间件丰富:持久化、DevTools、日志、undo 等。实战
// 1. 安装
npm install zustand
// 2. 创建 Store(store/useBearStore.js)
import { create } from 'zustand'
export const useBearStore = create((set) => ({
bears: 0,
// 同步修改
increase: () => set(state => ({ bears: state.bears + 1 })),
// 异步修改
fetchBears: async () => {
const res = await fetch('/api/bears').then(r => r.json())
set({ bears: res.count })
},
reset: () => set({ bears: 0 })
}))
// 3. 组件中使用
import { useBearStore } from './store/useBearStore'
function BearCounter() {
const bears = useBearStore(state => state.bears) // 仅订阅 bears,其他状态变化不重渲染
return <h1>{bears} bears</h1>
}
function Controls() {
const increase = useBearStore(state => state.increase)
const fetchBears = useBearStore(state => state.fetchBears)
return (
<>
<button onClick={increase}>+1</button>
<button onClick={fetchBears}>Fetch</button>
</>
)
}性能关键
// 只取需要的状态,精准订阅、最小化重渲染:
// ✅ 推荐:仅 bears 变化时更新
const bears = useBearStore(state => state.bears)
// ❌ 不推荐:状态任何变化都更新
const state = useBearStore(state => state)持久化
持久化(persist 中间件):数据存 localStorage
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export const useUserStore = create(
persist(
(set) => ({
token: '',
setToken: (t) => set({ token: t })
}),
{ name: 'user-storage' } // 存储 key
)
)调试
import { devtools } from 'zustand/middleware'
export const useStore = create(devtools((set) => ({...})))模块化目录
stores/
├── useUserStore.js
├── useCartStore.js
└── useTabStore.jsTS 支持
自动推导类型,无需手动写接口
import { create } from 'zustand'
interface CounterState {
count: number
inc: () => void
}
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
inc: () => set(s => ({ count: s.count + 1 }))
}))redux 是什么
Redux 是一个基于 JavaScript 的状态管理库,主要用于管理应用中共享状态
简单
# 三个核心原则
- 其一:单一状态树,所有数据都存储在唯一一个 store 中
- 其二:State 只读,不能直接修改 state,只能通过派发 action 变更。
- 其三:reducer 必须是 纯函数,接收 state 和 action,返回新的 state
# 五大核心概念
- 其一:State,全局状态仓库,存放所有共享数据,只读不可直接改。
- 其二:Action,描述「要做什么」的普通 JS 对象,固定结构:type(必选) + payload(载荷,携带参数),{ type: "ADD_COUNT", payload: 1 }
- 其三:Dispatch,唯一触发状态更新的方法,dispatch(action) 派发动作,交给 reducer 处理。
- 其四:Reducer,纯函数:(prevState, action) => newState,根据 action.type 判断,返回全新 state,不修改原 state,注意:不能异步、不能副作用
- 其五:Store,核心方法:getState() 获取状态 / dispatch() 派发更新 / subscribe() 订阅渲染
# 单向数据流流程
组件触发事件 → 调用 store.dispatch 派发 action → 触发 Reducer 计算并返回最新 state → Store 更新 → 组件中触发 store.subscribe 订阅渲染
# 模块化
通过 combineReducers 方法合并多个 Reducer 生成 rootReducer
# 问题来了
## Action 是什么?
答:它是一个固定结构的对象 { type: "ADD_COUNT", payload: 1 },描述修改哪个 state 和 改什么
## Reducer 是什么?
答:它是一个纯函数,不直接修改 state ,而是根据 action.type 判断,返回全新 state,
## Dispatch 是什么?
答:派发更新的函数,传入 action
## subscribe 是什么?
答:订阅函数,每次 store 状态变化,触发内部回调函数
# redux 的 subscribe 订阅,是 store 中任意一个状态变化都触发,还是只有当前依赖的状态变化触发?
答:全局任意字段更新 → 所有 subscribe 回调全部执行。(因为:Redux 全局只有一套订阅队列,dispatch 执行完成后,遍历执行所有订阅函数,无差异、无筛选)
# 接着问,subscribe 影响性能吗?怎么办?
答:特别影响,会造成大量无效计算、重复请求、频繁渲染。所以在实际开发中不可直接使用原生 subscribe,推荐和 react-redux 一起使用,react-redux 做了浅比较优化
# 接着问,react-redux 的 connect /useSelector 为什么不会全量刷新?
connect 内部二次做了浅层对比:
通过 mapStateToProps 拿到组件需要的局部状态,
新旧 props 浅比较,只有当前组件依赖的状态变了,才重新渲染,
其他无关 state 变化:subscribe 虽然触发,但对比无变化 → 不更新视图
# 为什么不用原生 Redux?
因为原生 Redux 太繁琐:
要写 actionType
要写 action
要写 reducer
要写 immutable 结构
要配中间件
要模块化合并
代码量巨大案例
目录
src/
├── store/
│ ├── index.js # store 配置文件
│ └── reducers.js # reducer 文件
└── components/
└── UserProfile.js # 上面的类组件第一步:定义 store 和 reducer
1. 在 store/index.js 中定义 store 模块化
import { createStore, combineReducers } from 'redux';
import { userReducer } from './reducers';
// 合并 reducer(虽然这里只有一个,但展示标准写法)
const rootReducer = combineReducers({
user: userReducer,
// 可以添加其他 reducer,比如 todos: todosReducer
});
// 创建 store
const store = createStore(rootReducer);
export default store;2. 在 store/reducers.js 定义 reducer
// 初始状态
const initialState = {
name: '张三',
age: 25,
email: 'zhangsan@example.com'
};
// user reducer
export const userReducer = (state = initialState, action) => {
switch (action.type) {
case 'SET_NAME':
return {
...state,
name: action.payload
};
case 'SET_AGE':
return {
...state,
age: action.payload
};
default:
return state;
}
};第二步:类组件中使用 redux
import React from 'react';
import store from './store';
class UserProfile extends React.Component {
constructor(props) {
super(props);
// 1. 初始化 state
this.state = {
user: store.getState().user,
};
// 2. 订阅 store,当 store 变化时,更新组件 state
this.unsubscribe = store.subscribe(() => {
this.setState({
user: store.getState().user,
});
});
}
componentWillUnmount() {
// 3. 取消订阅,防止内存泄漏
this.unsubscribe();
}
handleSetName = () => {
// 4. 事件触发,调用 dispatch 派发 action
store.dispatch({ type: 'SET_NAME', payload: 'New Name' });
};
render() {
const { user } = this.state;
return (
<div>
<p>{user.name}</p>
<button onClick={this.handleSetName}>修改</button>
</div>
);
}
}
export default UserProfile;第二步:函数组件中使用 redux
import React, { useState, useEffect } from 'react';
import store from './store';
function UserProfile() {
// 1. 初始化 state
const [user, setUser] = useState(store.getState().user);
// 2. 订阅 store(模拟生命周期),store 变化时更新 state
useEffect(() => {
const unsubscribe = store.subscribe(() => {
setUser(store.getState().user);
});
// 3. 取消订阅(模拟 componentWillUnmount), 空依赖数组,只在挂载时执行一次
return unsubscribe;
}, []);
const handleSetName = () => {
// 4. 事件触发,调用 dispatch 派发 action
store.dispatch({ type: 'SET_NAME', payload: 'New Name' });
};
return (
<div>
<p>{user.name}</p>
<button onClick={handleSetName}>修改</button>
</div>
);
}
export default UserProfile;reducer 限制
Redux 要求 reducer 是纯函数,不能直接修改状态。应使用扩展运算符(...)、Array.map()、Array.filter() 等方法创建新的状态副本:
// Reducer 必须是纯函数,不能包含 API 调用、定时器、日志打印等副作用,这些操作应放在 action 创建函数或中间件中。
// 错误做法:直接修改状态
state.todos.push(newTodo);
// 正确做法:返回新状态
return {
...state,
todos: [...state.todos, newTodo]
};React-Redux 是什么
react-redux 是连接 Redux 与 React 的官方绑定库(react-redux 包),让组件订阅 store、派发 action 更便捷
要点速览
# 为何使用 react-redux
1. 原生 subscribe 是全量订阅,任意 state 变更都会触发所有订阅回调,直接大量使用对性能影响较大。
2. 禁止业务中直接手写 subscribe;若必须使用,需手动缓存上一次状态,做字段对比,只在目标数据变化时执行逻辑。
3. connect 和 useSelector 优化
connect 内部默认对 mapStateToProps 返回结果做浅比较,所以类组件很少遇到这种重复渲染问题,问题主要集中在 函数组件 + useSelector,
useSelector 默认用 严格全等 === 对比,基础类型没问题,但是如果返回对象 / 数组,不做处理会频繁无效刷新;解决:搭配 shallowEqual 浅层比较。
import { useSelector, useDispatch, shallowEqual } from 'react-redux'
# 取出多个状态、返回对象
const { count, name, list } = useSelector(
state => ({
count: state.counter.count,
name: state.user.name,
list: state.user.list
}),
# 关键:开启浅层比较
shallowEqual
)
# ❌ 不加 shallowEqual,只要对象引用变化,必重渲染
const info = useSelector(state => ({
a: state.xxx.a,
b: state.xxx.b
}))
# react-redux 工作流程
1. 通过 Provider 将 Redux store 注入整个应用
2. 定义 store、reducer、actionTypes、actions
3. 在类组件中,通过 connect + mapStateToProps + mapDispatchToProps 映射状态和方法
4. 在函数组件中,通过 hook 函数 useSelector + useDispatch 获取 state,和 dispatch 方法派发 actionReact-Redux 示例
# 安装依赖
npm install redux react-redux
# 目录结构
src/
├── store/
│ ├── actionTypes.js # 动作类型
│ ├── actions.js # 动作创建函数
│ └── reducer.js # reducer 状态更新
├── store.js
├── CounterClass.js # 旧:class + connect
├── CounterFunc.js # 新:函数组件 + hooks
└── App.js
# 职责
actionTypes:类型常量
actions:触发动作
reducer:更新状态
store:唯一数据源
组件:展示页面
# 1. 在 store/actionTypes.js 中定义 action 类型(常量)
export const ADD = 'ADD';
export const SUBTRACT = 'SUBTRACT';
export const RESET = 'RESET';
# 2. 在 store/actions.js 中定义 action 固定类型结构 { type: ADD }
import { ADD, SUBTRACT, RESET } from './actionTypes';
export const add = () => ({ type: ADD });
export const subtract = () => ({ type: SUBTRACT });
export const reset = () => ({ type: RESET });
# 3. 在 store/reducer.js(状态处理)中定义 reducer
import { ADD, SUBTRACT, RESET } from './actionTypes';
const initialState = {
count: 0
};
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case ADD:
return { ...state, count: state.count + 1 };
case SUBTRACT:
return { ...state, count: state.count - 1 };
case RESET:
return { ...state, count: 0 };
default:
return state;
}
};
export default counterReducer;
# 4. 在 store.js 创建 store
import { createStore } from 'redux';
import counterReducer from './store/reducer';
const store = createStore(
counterReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
export default store;
# 5. 在 CounterClass.js 类组件中 + connect:mapStateToProps,mapDispatchToProps
import React from 'react';
import { connect } from 'react-redux';
import { add, subtract, reset } from './store/actions';
function CounterClass({ count, add, subtract, reset }) {
return (
<div style={{ margin: '20px 0' }}>
<h3>类组件 connect 写法</h3>
<p>count:{count}</p>
<button onClick={add}>+</button>
<button onClick={subtract} style={{margin:'0 8px'}}>-</button>
<button onClick={reset}>重置</button>
</div>
);
}
const mapStateToProps = state => ({ count: state.count }); ## 映射 state
const mapDispatchToProps = { add, subtract, reset }; ## 映射 方法
export default connect(mapStateToProps, mapDispatchToProps)(CounterClass);
# 6. 在 CounterFunc.js 函数组件中 + hooks:useSelector, useDispatch
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { add, subtract, reset } from './store/actions';
export default function CounterFunc() {
const count = useSelector(state => state.count); ## 读取全局 state
const dispatch = useDispatch(); ## 获取 dispatch
return (
<div style={{ margin: '20px 0' }}>
<h3>函数组件 Hooks 写法</h3>
<p>count:{count}</p>
{/* 派发 action */}
<button onClick={() => dispatch(add())}>+</button>
<button onClick={() => dispatch(subtract())} style={{margin:'0 8px'}}>-</button>
<button onClick={() => dispatch(reset())}>重置</button>
</div>
);
}
# 7. 在根组件 App.js 中,使用 Provider 组件 包裹根组件使整个应用中的所有组件都能访问到 Redux store
import { Provider } from 'react-redux';
import store from './store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);redux-thunk 异步中间件
redux-thunk 是 Redux 生态系统中最常用的中间件之一,它主要用于处理 Redux 中的异步操作。
要点速览
# 为什么 redux 无法实现异步逻辑?
Redux 的核心 reducer 必须是 纯函数,接收 state 和 action,返回新的 state。它不允许直接在 reducer 中写异步代码(如 fetch、axios 请求),因为这样会破坏可预测性。
所以,如果我们需要在 Redux 中处理异步(比如从服务器获取数据),就需要用 redux-thunk 中间件来管理这个过程。
# redux-thunk 的作用
默认 Redux 只能派发普通对象 action,redux-thunk 允许派发函数形式的 action,从而支持异步逻辑(定时器、接口请求)
函数形式的 action 默认接收 dispatch方法 和 getState方法 作为参数,可执行异步逻辑,根据异步操作的结果(成功或失败) dispatch 不同的 action
# redux-thunk 工作流程
组件 dispatch → thunk中间件拦截 → 执行异步函数 → 异步结束后再dispatch普通action → reducer更新state
# 常见的异步中间件
Redux 本身只能处理同步操作,异步逻辑(如 API 请求)需使用中间件:
- redux-thunk:处理简单异步逻辑(返回函数)
- redux-saga:处理复杂异步流程(如取消请求、重试)
- redux-observable:基于 RxJS 处理异步
# Thunk 和 Saga 底层原理
Thunk:中间件拦截 action,判断如果是函数就执行,并传入 dispatch
Saga:常驻后台监听 action,匹配到对应 type 后,执行 generator 异步流程
# Thunk 和 Saga 试用场景
Thunk:小型项目、简单异步、快速开发、团队入门
Saga:中大型项目、复杂异步(轮询、长连接、批量请求、搜索防抖、订单流程)React-Redux 示例
特点:组件只是负责渲染,异步业务逻辑放在 actions 中
# 安装依赖
npm i redux react-redux redux-thunk
# 目录结构
src/
├── store/
│ ├── actionTypes.js
│ ├── actions.js # 新增异步action
│ └── reducer.js
├── store.js # 引入thunk中间件
├── CounterClass.js
├── CounterFunc.js
└── App.js
# 1. 在 store/actionTypes.js 中定义 action 类型(常量)
export const ADD = 'ADD';
export const SUBTRACT = 'SUBTRACT';
export const RESET = 'RESET';
export const ADD_ASYNC = 'ADD_ASYNC'; # 新增异步类型
# 2. 在 store/actions.js 中定义 action 同步/异步,同步固定结构:{ type: ADD },异步固定结构:(dispatch) => { 执行完异步逻辑后 dispatch({ type: ADD })}
import { ADD, SUBTRACT, RESET, ADD_ASYNC } from './actionTypes';
## 同步action
export const add = () => ({ type: ADD });
export const subtract = () => ({ type: SUBTRACT });
export const reset = () => ({ type: RESET });
## 异步action:redux-thunk 写法
export const addAsync = () => {
## 返回函数,thunk自动注入dispatch
return (dispatch) => {
## 模拟接口请求 / 定时器异步
setTimeout(() => {
dispatch({ type: ADD_ASYNC });
}, 1000);
};
};
## 带参数的异步示例
export const addNumAsync = (num) => {
return (dispatch) => {
setTimeout(() => {
dispatch({ type: ADD, payload: num });
}, 1500);
};
};
# 3. 在 store/reducer.js(状态处理)中定义 reducer
import { ADD, SUBTRACT, RESET, ADD_ASYNC } from './actionTypes';
const initialState = {
count: 0
};
export default function counterReducer(state = initialState, action) {
switch (action.type) {
case ADD:
case ADD_ASYNC: ## 异步加和同步加共用逻辑
return { ...state, count: state.count + 1 };
case SUBTRACT:
return { ...state, count: state.count - 1 };
case RESET:
return { ...state, count: 0 };
default:
return state;
}
}
# 4. 在 store.js 创建 store,使用 redux-thunk 中间件
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import counterReducer from './store/reducer';
## 应用thunk中间件
const store = createStore(
counterReducer,
applyMiddleware(thunk),
## 开启 redux 调试工具
window.__REDUX_DEVTOOLS_EXTENSION__?.()
);
export default store;
# 5. 在 CounterClass.js 类组件中 + connect:mapStateToProps,mapDispatchToProps(在组件中同步与异步使用上无区别)
import React from 'react';
import { connect } from 'react-redux';
import { add, subtract, reset, addAsync, addNumAsync } from './store/actions';
function CounterClass({ count, add, subtract, reset, addAsync, addNumAsync }) {
return (
<div style={{ margin: '20px 0', padding: '20px', border: '1px solid #ccc' }}>
<h3>类组件 + connect + Thunk异步</h3>
<p>当前计数:{count}</p>
<button onClick={add}>同步+1</button>
<button onClick={subtract} style={{ margin: '0 8px' }}>同步-1</button>
<button onClick={reset}>重置</button>
<br /><br />
<button onClick={addAsync}>异步+1(1s后)</button>
<button onClick={() => addNumAsync(10)} style={{ marginLeft: '8px' }}>异步+10</button>
</div>
);
}
const mapStateToProps = state => ({ count: state.count });
## 异步action直接挂载即可,connect自动支持
const mapDispatchToProps = {
add,
subtract,
reset,
addAsync,
addNumAsync
};
export default connect(mapStateToProps, mapDispatchToProps)(CounterClass);
# 6. 在 CounterFunc.js 函数组件中 + hooks:useSelector, useDispatch(在组件中同步与异步使用上无区别)
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { add, subtract, reset, addAsync, addNumAsync } from './store/actions';
export default function CounterFunc() {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
return (
<div style={{ margin: '20px 0', padding: '20px', border: '1px solid #ccc' }}>
<h3>函数组件 + useDispatch + Thunk异步</h3>
<p>当前计数:{count}</p>
{/* 同步 */}
<button onClick={() => dispatch(add())}>同步+1</button>
<button onClick={() => dispatch(subtract())} style={{ margin: '0 8px' }}>同步-1</button>
<button onClick={() => dispatch(reset())}>重置</button>
<br /><br />
{/* 异步 */}
<button onClick={() => dispatch(addAsync())}>异步+1(1s后)</button>
<button onClick={() => dispatch(addNumAsync(5))} style={{ marginLeft: '8px' }}>异步+5(1.5s后)</button>
</div>
);
}
# 7. 在根组件 App.js 中,使用 Provider 组件 包裹根组件使整个应用中的所有组件都能访问到 Redux store(不变)
import { Provider } from 'react-redux';
import store from './store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);redux-saga 异步中间件
Redux-Saga 是一个用于处理 Redux 应用副作用(Side Effects,如异步请求、本地存储、路由跳转等)的中间件。它通过 ES6 Generator 函数 让异步流程更易读、易测试。
要点速览
# 什么是 Saga?
Saga 是一个 Generator 函数,它监听 Redux 的 action,当特定 action 被 dispatch 时,Saga 会执行相应的副作用逻辑。
# redux-saga 试用场景
✅ 复杂的异步流程
✅ 需要取消/并发控制
✅ 需要高可测试性
✅ 大型应用
# 核心 API
监听三兄弟:Every(全执行)、Latest(防抖)、Leading(节流)
异步两兄弟:Call(阻塞)、Fork(非阻塞)
控制流:All(并行)、Race(竞赛)、Join(等)、Cancel(取消)
数据:Take(收 action)、Put(发 action)、Select(取 state)
时间:Delay(延迟)、Throttle(节流)、Debounce(防抖)
# 易混区分 API
takeEvery vs takeLatest:Every 并发;Latest 取消旧的
call vs fork:Call 阻塞;Fork 后台并行
fork vs spawn:fork 父子关联;spawn 完全独立
take vs takeEvery:take 一次;takeEvery 循环监听redux-saga 核心 API
# 基础 Effect(最常用,建议掌握)
take(pattern):暂停,等待匹配的 action 到来(一次)
put(action):派发 action,等同于 dispatch
call(fn, ...args):阻塞调用异步函数(Promise),串行
fork(fn, ...args):非阻塞后台任务,并行,返回 Task
select(selector?, ...args):获取 redux state,select() 取整个 state
delay(ms):延迟指定毫秒(内部封装 Promise)Redux-Saga
# 监听类(高级监听,业务常见)
takeEvery(pattern, saga):每次匹配都执行,允许并发
takeLatest(pattern, saga):防抖,取消上一次,只留最后一次
takeLeading(pattern, saga):节流,第一次执行,期间忽略后续
throttle(ms, pattern, saga):节流(时间窗口),指定时间内只一次
debounce(ms, pattern, saga):防抖(延迟执行),最后一次触发后延迟执行
# 控制流(并行 / 竞赛 / 批量)
all([effect1, effect2]):并行执行所有 effect,全部完成才返回
race([effect1, effect2]):竞赛模式,谁先完成取谁结果,其余自动取消
join(task):等待 fork 的任务结束(阻塞)
cancel(task):取消 fork 的任务(包括内部所有子任务)
cancelled():判断当前任务是否被取消(内部用)
# 任务管理(Task 相关)
fork → 返回 Task 对象:含 id、isRunning()、cancel() 等
spawn(fn, ...args):独立子任务,父取消不影响子(隔离)
setContext(props):设置任务上下文
getContext(key):获取任务上下文
# 通道与流处理(进阶,事件队列)
channel():创建自定义通道,缓存事件
actionChannel(pattern):action 队列化,控制并发数(如一次只处理 1 个请求)
flush(channel):清空通道,取出所有缓存事件
take(channel):从通道取事件
put(channel, value):往通道放事件
# 错误处理(健壮性必备)
try / catch / finally:捕获 call/put 等异常
onError:全局错误监听(中间件配置)
cancel(task):异常时取消任务
# 创建中间件(入口 API)
createSagaMiddleware(options):创建 saga 中间件实例
middleware.run(rootSaga, ...args):启动根 sagaEffects API 示例
1. take / takeEvery / takeLatest
import { take, takeEvery, takeLatest } from 'redux-saga/effects';
// take:等待一次,阻塞直到指定 action 被 dispatch
function* watchLogin() {
while (true) {
const action = yield take('LOGIN_REQUEST');
// 只处理一次,需要配合循环
yield call(handleLogin, action);
}
}
// takeEvery:每次匹配都执行,允许并发
yield takeEvery('FETCH_DATA', fetchData);
// takeLatest:只执行最后一次,之前的会被取消
yield takeLatest('SEARCH', searchAPI); // 防止快速多次搜索2. call / apply
import { call, apply } from 'redux-saga/effects';
// call:调用函数
const user = yield call(fetchUser, userId);
// 调用对象方法
const data = yield apply(context, obj.method, [args]);
// call 也可以调用 Promise
const response = yield call(fetch, '/api/users');3. put
import { put } from 'redux-saga/effects';
// 相当于 dispatch
yield put({ type: 'ACTION_TYPE', payload: data });
// 批量 dispatch
yield put.all([
put(action1()),
put(action2())
]);4. select
import { select } from 'redux-saga/effects';
// 获取整个 state
const state = yield select();
// 获取部分 state
const user = yield select(state => state.user);
// 在异步流程中使用当前 state
function* updateUser() {
const userId = yield select(state => state.auth.userId);
yield call(fetchUser, userId);
}5. fork / spawn
import { fork, spawn } from 'redux-saga/effects';
// fork:非阻塞执行任务
function* mainSaga() {
yield fork(watchFetchUser); // 不等待,继续执行
yield fork(watchUpdateUser);
}
// spawn:类似 fork,但任务独立于父级(错误不会影响父级)
yield spawn(backgroundTask);6. cancel
import { cancel, take } from 'redux-saga/effects';
function* backgroundTask() {
while (true) {
yield delay(1000);
console.log('Running...');
}
}
function* main() {
const task = yield fork(backgroundTask);
// 等待 CANCEL_TASK action
yield take('CANCEL_TASK');
// 取消后台任务
yield cancel(task);
}7. race
import { race, call, delay } from 'redux-saga/effects';
function* fetchWithTimeout() {
const { data, timeout } = yield race({
data: call(fetchData),
timeout: delay(5000) // 5 秒超时
});
if (data) {
console.log('请求成功', data);
} else if (timeout) {
console.log('请求超时');
}
}8. all
import { all, call } from 'redux-saga/effects';
// 并发执行多个任务,等待全部完成
function* loadInitialData() {
const [users, posts, comments] = yield all([
call(fetchUsers),
call(fetchPosts),
call(fetchComments)
]);
yield put(initSuccess({ users, posts, comments }));
}redux-thunk 基础案例
要点速览
# 相比于 thunk
1. 多了一个 sagas.js 核心异步文件:因为异步逻辑不在 actions.js 中,而是 sagas.js 中,每次派发 action 在 sagas.js 中都会监听到对应的 action 变化执行对应的异步逻辑
2. saga 的 actions.js 中,异步和同步的写法相同
3. 在 store.js 使用 redux-saga 中间件,相比于 thunk 还要执行 run rootSaga,rootSaga 为 sagas.js 导出的总 generator 函数
# 执行过程
每次派发 action 在 sagas.js 中都会监听到对应的 action 变化执行对应的异步逻辑React-Redux 示例
特点:组件只是负责渲染,异步业务逻辑放在 sagas 中,action 依旧是同步写法
# 安装依赖
npm i redux react-redux redux-saga
# 目录结构
src
├── store
│ ├── actionTypes.js
│ ├── actions.js
│ ├── reducer.js
│ └── sagas.js # saga 核心逻辑
├── store.js
├── CounterFunc.js ## 函数组件
├── CounterClass.js ## 类组件
└── App.js
# 1. 在 store/actionTypes.js 中定义 action 类型(常量)
## 同步
export const ADD = 'ADD';
export const SUBTRACT = 'SUBTRACT';
export const RESET = 'RESET';
## Saga 异步指令
export const ADD_ASYNC = 'ADD_ASYNC';
export const FETCH_DATA = 'FETCH_DATA';
# 2. 在 store/actions.js 中定义 action 同步/异步,都是原同步写法,固定结构:{ type: ADD }
import { ADD, SUBTRACT, RESET, ADD_ASYNC, FETCH_DATA } from './actionTypes';
## 同步 action
export const add = () => ({ type: ADD });
export const subtract = () => ({ type: SUBTRACT });
export const reset = () => ({ type: RESET });
## Saga 监听 action
export const addAsync = () => ({ type: ADD_ASYNC });
export const fetchData = () => ({ type: FETCH_DATA });
# 3. 在 store/reducer.js(状态处理)中定义 reducer
import { ADD, SUBTRACT, RESET } from './actionTypes';
const initialState = {
count: 0,
info: null
};
export default function reducer(state = initialState, action) {
switch (action.type) {
case ADD:
return { ...state, count: state.count + 1 };
case SUBTRACT:
return { ...state, count: state.count - 1 };
case RESET:
return { ...state, count: 0 };
case 'FETCH_SUCCESS':
return { ...state, info: action.payload };
default:
return state;
}
}
# 4. 在 store/sagas.js 中定义处理核心异步逻辑
import { takeEvery, put, delay, call } from 'redux-saga/effects';
import { ADD_ASYNC, FETCH_DATA, ADD } from './actionTypes';
## 异步+1
function* handleAddAsync() {
yield delay(1000);
yield put({ type: ADD });
}
## 模拟接口
const getInfo = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1');
return res.json();
};
## 异步请求
function* handleFetchData() {
const res = yield call(getInfo);
yield put({ type: 'FETCH_SUCCESS', payload: res });
}
## 根监听
export default function* rootSaga() {
yield takeEvery(ADD_ASYNC, handleAddAsync);
yield takeEvery(FETCH_DATA, handleFetchData);
}
# 5. 在 store.js 创建 store,使用 redux-saga 中间件,相比于 thunk 还要执行 run rootSaga
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import reducer from './store/reducer';
import rootSaga from './store/sagas';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware),
window.__REDUX_DEVTOOLS_EXTENSION__?.()
);
sagaMiddleware.run(rootSaga);
export default store;
# 6. 在 CounterClass.js 类组件中 + connect:mapStateToProps,mapDispatchToProps(在组件中同步与异步使用上无区别)
import React from 'react';
import { connect } from 'react-redux';
import { add, subtract, reset, addAsync, fetchData } from './store/actions';
class CounterClass extends React.Component {
render() {
const { count, info, add, subtract, reset, addAsync, fetchData } = this.props;
return (
<div style={{ border: '#ccc 1px solid', padding: 20, margin: 10 }}>
<h3>类组件 — Redux Saga</h3>
<p>计数:{count}</p>
<button onClick={add}>同步 +1</button>
<button onClick={subtract} style={{ margin: '0 6px' }}>同步 -1</button>
<button onClick={reset}>重置</button>
<br /><br />
<button onClick={addAsync}>Saga 异步+1(1s)</button>
<button onClick={fetchData} style={{ marginLeft: 6 }}>Saga 请求数据</button>
{info && <pre>{JSON.stringify(info, null, 2)}</pre>}
</div>
)
}
}
const mapStateToProps = state => ({
count: state.count,
info: state.info
});
const mapDispatchToProps = {
add,
subtract,
reset,
addAsync,
fetchData
};
export default connect(mapStateToProps, mapDispatchToProps)(CounterClass);
# 7. 在 CounterFunc.js 函数组件中 + hooks:useSelector, useDispatch(在组件中同步与异步使用上无区别)
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { add, subtract, reset, addAsync, fetchData } from './store/actions';
export default function CounterFunc() {
const { count, info } = useSelector(state => state);
const dispatch = useDispatch();
return (
<div style={{ border: '#ccc 1px solid', padding: 20, margin: 10 }}>
<h3>函数组件 — Redux Saga</h3>
<p>计数:{count}</p>
<button onClick={() => dispatch(add())}>同步 +1</button>
<button onClick={() => dispatch(subtract())} style={{ margin: '0 6px' }}>同步 -1</button>
<button onClick={() => dispatch(reset())}>重置</button>
<br /><br />
<button onClick={() => dispatch(addAsync())}>Saga 异步+1(1s)</button>
<button onClick={() => dispatch(fetchData())} style={{ marginLeft: 6 }}>Saga 请求数据</button>
{info && <pre>{JSON.stringify(info, null, 2)}</pre>}
</div>
);
}
# 8. 在根组件 App.js 中,使用 Provider 组件 包裹根组件使整个应用中的所有组件都能访问到 Redux store(不变)
import { Provider } from 'react-redux';
import store from './store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);redux-saga 用实战案例
延迟异步、请求接口、防抖、节流、轮询、取消异步任务、并发请求、监听多个 action
要点速览
在组件中只需要派发最后一个 take 的 action 即可,执行异步中的所有任务前置
先统一:actionTypes
// 基础
export const ADD = 'ADD';
export const FETCH_DATA = 'FETCH_DATA';
// 常用案例
export const FETCH_LATEST = 'FETCH_LATEST'; // 防抖
export const FETCH_LEADING = 'FETCH_LEADING'; // 节流
export const START_POLLING = 'START_POLLING'; // 轮询
export const STOP_POLLING = 'STOP_POLLING';
export const CANCEL_TASK = 'CANCEL_TASK';
export const PARALLEL_TASKS = 'PARALLEL_TASKS'; // 并发1、延迟异步(最基础)
import { takeEvery, put, delay } from 'redux-saga/effects';
function* asyncAdd() {
yield delay(1000); // 1. 先延迟等待
yield put({ type: 'ADD' }); // 2. 派发 action
}
function* watchAsyncAdd() {
yield takeEvery('ADD_ASYNC', asyncAdd); // 3. 每次都执行
}2、请求接口(最常用)
import { call, put } from 'redux-saga/effects';
const fetchApi = () =>
fetch('https://jsonplaceholder.typicode.com/todos/1').then(res => res.json());
function* fetchData() {
try {
const data = yield call(fetchApi); // 1. 调用异步函数
yield put({ type: 'FETCH_SUCCESS', payload: data }); // 2. 派发 action
} catch (err) {
yield put({ type: 'FETCH_ERROR', payload: err }); // 3. 每次都执行
}
}
function* watchFetch() {
yield takeEvery('FETCH_DATA', fetchData);
}
// 关键:在组件中只需要派发 FETCH_DATA 即可
import { fetchData } from './actions';
const dispatch = useDispatch();
// 手动触发 Saga 请求数据
const handleFetchData = () => {
dispatch(fetchData()); // 派发 FETCH_DATA action
};
// 自动加载(可选)
useEffect(() => {
dispatch(fetchData());
}, []);3、防抖(只执行最后一次)搜索必备
import { takeLatest } from 'redux-saga/effects';
function* fetchLatest() {
const data = yield call(() => fetch('https://jsonplaceholder.typicode.com/todos').then(res => res.json())); // 1. 调用异步函数
yield put({ type: 'FETCH_SUCCESS', payload: data }); // 2. 派发 action
}
function* watchLatest() {
yield takeLatest('FETCH_LATEST', fetchLatest); // 3. 防抖执行
}4、节流(只执行第一次)防重复点击
import { takeLeading } from 'redux-saga/effects';
function* fetchLeading() {
const data = yield call(() => fetch('https://jsonplaceholder.typicode.com/todos').then(res => res.json())); // 1. 调用异步函数
yield put({ type: 'FETCH_SUCCESS', payload: data }); // 2. 派发 action
}
function* watchLeading() {
yield takeLeading('FETCH_LEADING', fetchLeading); // 3. 节流执行
}5、轮询(自动刷新)订单 / 消息 / 数据
import { take, cancel, fork, delay, put } from 'redux-saga/effects';
function* pollingTask() {
while (true) {
const data = yield call(() => fetch('https://jsonplaceholder.typicode.com/todos/1').then(res => res.json())); // 1. 调用异步函数
yield put({ type: 'POLL_SUCCESS', payload: data }); // 2. 派发 action
yield delay(3000); // 3. 延迟 3 秒,再重新执行(每 3 秒刷新一次)
}
}
function* watchPolling() {
let task;
while (true) {
yield take('START_POLLING'); // 4. 开始轮训,等待一次,阻塞直到指定 action 被 dispatch
task = yield fork(pollingTask); // 5. 创建非阻塞后台子任务
yield take('STOP_POLLING'); // 6. 开始轮训,等待一次,阻塞直到指定 action 被 dispatch
yield cancel(task); // 7. 取消后台轮训子任务
}
}6、取消异步任务
import { take, cancel, fork } from 'redux-saga/effects';
function* longTask() {
yield delay(10000); // 1. 调延迟 10 秒
yield put({ type: 'TASK_FINISH' }); // 2. 派发 action
}
function* watchCancel() {
const task = yield take('CANCEL_TASK'); // 3. 取消后台任务,等待一次,阻塞直到指定 action 被 dispatch
yield cancel(task); // 4. 取消后台任务
}7、并发请求(同时发多个接口)
import { all, call, put } from 'redux-saga/effects';
const fetch1 = () => fetch('https://jsonplaceholder.typicode.com/todos/1').then(res => res.json());
const fetch2 = () => fetch('https://jsonplaceholder.typicode.com/todos/2').then(res => res.json());
function* parallelTasks() {
const [data1, data2] = yield all([
call(fetch1), // 1. 调用接口 1
call(fetch2) // 2. 调用接口 2
]);
yield put({ type: 'PARALLEL_SUCCESS', payload: [data1, data2] }); // 3. 派发 action
}
function* watchParallel() {
yield takeEvery('PARALLEL_TASKS', parallelTasks); // 4. 每次匹配都执行,允许并发
}8、监听多个 action
import { takeEvery } from 'redux-saga/effects';
function* handleMulti() {
console.log('触发了多个 action 中的一个');
}
function* watchMulti() {
yield takeEvery(['ACTION1', 'ACTION2', 'ACTION3'], handleMulti); // 1. 每次匹配都执行,允许并发
}最终完整 rootSaga
import { all } from 'redux-saga/effects';
import { watchAsyncAdd, watchFetch, watchLatest, watchLeading, watchPolling, watchCancel, watchParallel, watchMulti } from './sagaWatches';
export default function* rootSaga() {
yield all([
watchAsyncAdd(),
watchFetch(),
watchLatest(),
watchLeading(),
watchPolling(),
watchCancel(),
watchParallel(),
watchMulti()
]);
}redux-saga 企业案例
要点速览
案例
第一步:安装依赖
npm i redux react-redux redux-saga
第二步:统一 actionTypes.js
// 同步
export const ADD = "ADD";
// 基础异步
export const ADD_ASYNC = "ADD_ASYNC";
export const FETCH_DATA = "FETCH_DATA";
// 防抖/节流
export const FETCH_LATEST = "FETCH_LATEST";
export const FETCH_LEADING = "FETCH_LEADING";
// 轮询
export const START_POLLING = "START_POLLING";
export const STOP_POLLING = "STOP_POLLING";
// 任务取消
export const START_LONG_TASK = "START_LONG_TASK";
export const CANCEL_LONG_TASK = "CANCEL_LONG_TASK";
// 并发请求
export const FETCH_PARALLEL = "FETCH_PARALLEL";
// 登录鉴权
export const LOGIN = "LOGIN";
export const LOGOUT = "LOGOUT";
export const REFRESH_TOKEN = "REFRESH_TOKEN";
// 结果回调type
export const FETCH_SUCCESS = "FETCH_SUCCESS";
export const FETCH_ERROR = "FETCH_ERROR";
export const POLL_SUCCESS = "POLL_SUCCESS";
export const PARALLEL_SUCCESS = "PARALLEL_SUCCESS";
export const LOGIN_SUCCESS = "LOGIN_SUCCESS";
export const LOGIN_FAIL = "LOGIN_FAIL";第三步:统一 actions.js
// 同步
export const add = () => ({ type: ADD });
// 基础异步
export const addAsync = () => ({ type: ADD_ASYNC });
export const fetchData = () => ({ type: FETCH_DATA });
// 防抖节流
export const fetchLatest = (kw) => ({ type: FETCH_LATEST, payload: kw });
export const fetchLeading = () => ({ type: FETCH_LEADING });
// 轮询
export const startPolling = () => ({ type: START_POLLING });
export const stopPolling = () => ({ type: STOP_POLLING });
// 任务取消
export const startLongTask = () => ({ type: START_LONG_TASK });
export const cancelLongTask = () => ({ type: CANCEL_LONG_TASK });
// 并发
export const fetchParallel = () => ({ type: FETCH_PARALLEL });
// 登录
export const login = (params) => ({ type: LOGIN, payload: params });
export const logout = () => ({ type: LOGOUT });
export const refreshToken = () => ({ type: REFRESH_TOKEN });第四步:完整 sagas.js(所有案例集成)
import {
takeEvery, takeLatest, takeLeading,
put, call, delay, fork, cancel, take, all
} from "redux-saga/effects";
import * as Types from "./actionTypes";
// 公共请求方法
const request = (url) => fetch(url).then(res => res.json());
// 1. 基础延迟异步
function* handleAddAsync() {
yield delay(1000);
yield put({ type: Types.ADD });
}
// 2. 基础接口请求 + 异常捕获
function* handleFetchData() {
try {
const res = yield call(request, "https://jsonplaceholder.typicode.com/todos/1");
yield put({ type: Types.FETCH_SUCCESS, payload: res });
} catch (err) {
yield put({ type: Types.FETCH_ERROR, payload: err.message });
}
}
// 3. takeLatest 防抖(搜索框连续输入)
function* handleFetchLatest(action) {
yield delay(500); // 防抖间隔
const res = yield call(request, "https://jsonplaceholder.typicode.com/todos");
yield put({ type: Types.FETCH_SUCCESS, payload: { kw: action.payload, list: res } });
}
// 4. takeLeading 节流(防按钮重复点击)
function* handleFetchLeading() {
const res = yield call(request, "https://jsonplaceholder.typicode.com/todos/2");
yield put({ type: Types.FETCH_SUCCESS, payload: res });
}
// 5. 轮询 + 手动停止
function* pollingTask() {
while (true) {
const res = yield call(request, "https://jsonplaceholder.typicode.com/todos/3");
yield put({ type: Types.POLL_SUCCESS, payload: res });
yield delay(3000);
}
}
function* handlePolling() {
const task = yield fork(pollingTask);
yield take(Types.STOP_POLLING);
yield cancel(task);
}
// 6. 长任务 + 主动取消
function* longTask() {
yield delay(10000);
yield put({ type: "TASK_END", payload: "长任务执行完成" });
}
function* handleCancelTask() {
const task = yield fork(longTask);
yield take(Types.CANCEL_LONG_TASK);
yield cancel(task);
}
// 7. all 并发请求
function* handleParallel() {
const [res1, res2] = yield all([
call(request, "https://jsonplaceholder.typicode.com/todos/1"),
call(request, "https://jsonplaceholder.typicode.com/todos/2")
]);
yield put({ type: Types.PARALLEL_SUCCESS, payload: [res1, res2] });
}
// 8. 登录 + Token 存储
const loginApi = (params) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (params.account) resolve({ token: "saga-token-2026", name: "admin" });
else reject("账号不能为空");
}, 800);
});
};
function* handleLogin(action) {
try {
const res = yield call(loginApi, action.payload);
localStorage.setItem("token", res.token);
yield put({ type: Types.LOGIN_SUCCESS, payload: res });
} catch (err) {
yield put({ type: Types.LOGIN_FAIL, payload: err });
}
}
// 9. 刷新 Token 单独监听
function* handleRefreshToken() {
yield delay(1000);
localStorage.setItem("token", "new-saga-token");
}
// 根监听:统一绑定
export default function* rootSaga() {
yield all([
takeEvery(Types.ADD_ASYNC, handleAddAsync),
takeEvery(Types.FETCH_DATA, handleFetchData),
takeLatest(Types.FETCH_LATEST, handleFetchLatest),
takeLeading(Types.FETCH_LEADING, handleFetchLeading),
takeEvery(Types.START_POLLING, handlePolling),
takeEvery(Types.START_LONG_TASK, handleCancelTask),
takeEvery(Types.FETCH_PARALLEL, handleParallel),
takeEvery(Types.LOGIN, handleLogin),
takeEvery(Types.REFRESH_TOKEN, handleRefreshToken),
takeEvery(Types.LOGOUT, function* () {
localStorage.removeItem("token");
})
]);
}第五步:reducer.js
import * as Types from "./actionTypes";
const initialState = {
count: 0,
apiData: null,
pollData: null,
parallelData: null,
user: null,
loginErr: ""
};
export default function reducer(state = initialState, action) {
switch (action.type) {
case Types.ADD:
return { ...state, count: state.count + 1 };
case Types.FETCH_SUCCESS:
return { ...state, apiData: action.payload };
case Types.FETCH_ERROR:
return { ...state, apiData: action.payload };
case Types.POLL_SUCCESS:
return { ...state, pollData: action.payload };
case Types.PARALLEL_SUCCESS:
return { ...state, parallelData: action.payload };
case Types.LOGIN_SUCCESS:
return { ...state, user: action.payload, loginErr: "" };
case Types.LOGIN_FAIL:
return { ...state, loginErr: action.payload };
case Types.LOGOUT:
return { ...state, user: null };
default:
return state;
}
}第六步:store.js(仅挂载 saga)
import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import reducer from "./store/reducer";
import rootSaga from "./store/sagas";
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware),
window.__REDUX_DEVTOOLS_EXTENSION__?.()
);
sagaMiddleware.run(rootSaga);
export default store;第七步:类组件 CounterClass.js 中
import React from "react";
import { connect } from "react-redux";
import {
add, addAsync, fetchData, fetchLatest, fetchLeading,
startPolling, stopPolling, startLongTask, cancelLongTask,
fetchParallel, login, logout, refreshToken
} from "./store/actions";
class CounterClass extends React.Component {
render() {
const {
count, apiData, pollData, parallelData, user,
add, addAsync, fetchData, fetchLatest
} = this.props;
return (
<div style={{border:"1px solid #ccc",padding:20,margin:10}}>
<h3>类组件|Saga 全案例</h3>
<p>计数器:{count}</p>
<button onClick={add}>同步+1</button>
<button onClick={addAsync} style={{marginLeft:6}}>延迟+1</button>
<div style={{margin:"10px 0"}}>
<button onClick={fetchData}>基础请求</button>
<button onClick={()=>fetchLatest("saga")} style={{marginLeft:6}}>防抖搜索</button>
</div>
<div>
<button onClick={startPolling}>开启轮询</button>
<button onClick={stopPolling} style={{marginLeft:6}}>停止轮询</button>
</div>
<div style={{margin:"10px 0"}}>
<button onClick={startLongTask}>开启长任务</button>
<button onClick={cancelLongTask} style={{marginLeft:6}}>取消任务</button>
</div>
<button onClick={fetchParallel}>并发请求</button>
<div style={{marginTop:10}}>
<button onClick={()=>login({account:"admin"})}>登录</button>
<button onClick={refreshToken} style={{marginLeft:6}}>刷新Token</button>
<button onClick={logout} style={{marginLeft:6}}>退出</button>
</div>
{user && <p>当前用户:{user.name}</p>}
</div>
)
}
}
const mapStateToProps = state => ({
count: state.count,
apiData: state.apiData,
pollData: state.pollData,
parallelData: state.parallelData,
user: state.user
});
const mapDispatchToProps = {
add, addAsync, fetchData, fetchLatest, fetchLeading,
startPolling, stopPolling, startLongTask, cancelLongTask,
fetchParallel, login, logout, refreshToken
};
export default connect(mapStateToProps, mapDispatchToProps)(CounterClass);第八步:函数组件 CounterFunc.js
import { useSelector, useDispatch } from "react-redux";
import {
add, addAsync, fetchData, fetchLatest, fetchLeading,
startPolling, stopPolling, startLongTask, cancelLongTask,
fetchParallel, login, logout, refreshToken
} from "./store/actions";
export default function CounterFunc() {
const state = useSelector(s => s);
const dispatch = useDispatch();
return (
<div style={{border:"1px solid #ccc",padding:20,margin:10}}>
<h3>函数组件|Saga 全案例</h3>
<p>计数器:{state.count}</p>
<button onClick={()=>dispatch(add())}>同步+1</button>
<button onClick={()=>dispatch(addAsync())} style={{marginLeft:6}}>延迟+1</button>
<div style={{margin:"10px 0"}}>
<button onClick={()=>dispatch(fetchData())}>基础请求</button>
<button onClick={()=>dispatch(fetchLatest("hook"))} style={{marginLeft:6}}>防抖搜索</button>
</div>
<button onClick={()=>dispatch(fetchParallel())}>并发请求</button>
</div>
);
}第九步:在 App.js 根组件中
import { Provider } from 'react-redux';
import store from './store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);redux-saga 架构目录
项目目录
src/
├── store
│ ├── modules // 模块化拆分(大型项目必备)
│ │ ├── counter.js
│ │ ├── user.js
│ │ └── request.js
│ ├── actionTypes // 按模块拆分常量
│ │ ├── counter.js
│ │ └── user.js
│ ├── actions // 模块化action
│ ├── reducers // 拆分reducer + combineReducers
│ ├── sagas
│ │ ├── modules // 业务saga拆分
│ │ │ ├── counterSaga.js
│ │ │ └── userSaga.js
│ │ ├── commonSaga.js // 公共:全局拦截、错误处理
│ │ └── rootSaga.js // 合并所有saga
│ ├── middlewares // 自定义中间件
│ └── index.js // 统一导出store
├── utils
│ ├── request.js // 统一请求封装
│ └── token.js // token工具
└── pages // 业务页面redux-saga 原理
要点速览
# 底层原理
基于 ES6 Generator 生成器函数 + 发布订阅
中间件常驻内存,全局监听所有 dispatch 的 action
匹配 action.type 后,暂停 / 恢复异步流程,统一管理副作用
# 核心流程
组件dispatch action → saga中间件拦截 →
匹配takeEvery/takeLatest监听 →
执行generator异步逻辑 →
异步结束 yield put 派发同步action →
reducer 更新state
# Saga vs Thunk 简明对照
Thunk:简单、学习成本低,适合小型项目;副作用散落在 action
Saga:集中管理副作用、天然支持 防抖 / 节流 / 取消 / 轮询 / 并发,适合中大型复杂项目
# Generator & Yield 作用
yield:暂停函数执行
中间件接管下一步执行,实现异步同步化写法redux-saga 模块化
要点速览
# 1. 每个业务 维护一个 独立 saga 文件
- counter 业务 → counterSaga.js
- user 业务 → userSaga.js
- order 业务 → orderSaga.js
# 2. rootSaga 只做一件事:合并 saga
yield all([
watchA(),
watchB(),
watchC()
])
# 3. reducer 也需要同步模块(架构要求)
一个业务模块 = 一套完整的:actionType + action + reducer + saga案例
目录结构
src/
└── store/
├── actionTypes/ # 按模块放类型
│ ├── counter.js
│ └── user.js
├── reducers/ # reducer 模块化
│ ├── counterReducer.js
│ ├── userReducer.js
│ └── index.js # combineReducers 合并
├── sagas/ # saga 模块化
│ ├── modules/
│ │ ├── counterSaga.js
│ │ └── userSaga.js
│ └── rootSaga.js
└── index.js # 创建 store1、业务-计数 reducer
store/reducers/counterReducer.js
import * as Types from '../actionTypes/counter';
const initialState = {
count: 0
};
export default function counterReducer(state = initialState, action) {
switch (action.type) {
case Types.ADD:
return { ...state, count: state.count + 1 };
default:
return state;
}
}2、业务-用户 reducer
store/reducers/userReducer.js
import * as Types from '../actionTypes/user';
const initialState = {
info: null,
token: ''
};
export default function userReducer(state = initialState, action) {
switch (action.type) {
case Types.USER_LOGIN_SUCCESS:
return { ...state, token: action.payload.token };
case Types.USER_INFO_SUCCESS:
return { ...state, info: action.payload };
default:
return state;
}
}3、合并 reducer
store/reducers/index.js
import { combineReducers } from 'redux';
import counterReducer from './counterReducer';
import userReducer from './userReducer';
const rootReducer = combineReducers({
counter: counterReducer,
user: userReducer,
});
export default rootReducer;4、业务-计数saga
store/sagas/modules/counterSaga.js
import { takeEvery, put, delay } from 'redux-saga/effects';
// 异步任务
function* workAddAsync() {
yield delay(1000);
yield put({ type: 'counter/add' });
}
// 监听
function* watchCounter() {
yield takeEvery('counter/addAsync', workAddAsync);
}
export default watchCounter;5、业务-用户saga
store/sagas/modules/userSaga.js
import { takeEvery, takeLatest, put, call, delay } from "redux-saga/effects";
import * as Types from "../../actionTypes/user";
// ---------- 模拟接口 ----------
const loginApi = (params) => new Promise(resolve => {
setTimeout(() => resolve({ token: "xxx-xxx", name: "admin" }), 600);
});
const getUserInfoApi = () => new Promise(resolve => {
setTimeout(() => resolve({ id: 1, role: "admin" }), 400);
});
const refreshTokenApi = () => new Promise(resolve => {
setTimeout(() => resolve({ newToken: "new-xxx" }), 300);
});
// ---------- 1. 登录 异步流程 ----------
function* handleLogin(action) {
try {
const res = yield call(loginApi, action.payload);
yield put({ type: Types.USER_LOGIN_SUCCESS, payload: res });
} catch (err) {
console.log("登录失败:", err);
}
}
// ---------- 2. 获取用户信息 异步流程 ----------
function* handleGetUserInfo() {
const res = yield call(getUserInfoApi);
yield put({ type: Types.USER_INFO_SUCCESS, payload: res });
}
// ---------- 3. 刷新Token 异步流程 ----------
function* handleRefreshToken() {
const res = yield call(refreshTokenApi);
console.log("刷新完成", res);
}
// ---------- 当前模块:统一监听(核心) ----------
function* watchUserSaga() {
// 登录:takeLatest 防抖,防止重复点击
yield takeLatest(Types.USER_LOGIN, handleLogin);
// 获取用户信息:正常监听
yield takeEvery(Types.USER_GET_INFO, handleGetUserInfo);
// 刷新token
yield takeEvery(Types.USER_REFRESH_TOKEN, handleRefreshToken);
}
// 只导出当前模块总监听
export default watchUserSaga;6、合并 saga
store/sagas/rootSaga.js 把所有模块化 saga 全部合并
import { all } from 'redux-saga/effects';
import watchCounter from './modules/counterSaga';
import watchUser from './modules/userSaga';
export default function* rootSaga() {
yield all([
watchCounter(),
watchUser(),
// 继续加更多模块...
]);
}7、store/index.js(创建 store)
import { createStore, applyMiddleware, combineReducers } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas/rootSaga';
// 假的 reducer(演示用)
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case 'counter/add': return { count: state.count + 1 };
default: return state;
}
};
const userReducer = (state = { info: null }, action) => {
switch (action.type) {
case 'user/save': return { info: action.payload };
default: return state;
}
};
const rootReducer = combineReducers({
counter: counterReducer,
user: userReducer,
});
// saga
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
export default store;Redux Toolkit(RTK) 是什么
要点速览
# Redux Toolkit 是什么?
Redux Toolkit(RTK)是官方推荐、开箱即用、简化 90% 代码的 Redux 标准工具集
# 内置的API
createSlice 创建"切片",自动生成 action creators 和 reducer
configureStore(自动合并 reducer + 中间件 + DevTools)
createAsyncThunk(异步请求) 处理异步操作(如 API 请求),自动生成 pending/fulfilled/rejected 三种状态的 action
createSelector(缓存选择器)
默认集成 immer(直接修改 state)
默认集成 redux-thunk
一句话:写更少代码,做更多事,企业标准。
# RTK 的作用
为了解决传统 Redux 写法有几个痛点:
- 样板代码多:action type、action creator、reducer 要分开写。
- 需要手动配置:中间件(如 redux-thunk)、devtools 等要自己加。
- 不可变性处理麻烦:要写 ...state 或用 immer。
- 异步逻辑复杂:thunk 要自己写。
# RTX 优势
- 减少样板代码:action type、creator 自动生成
- 简化不可变更新:直接 “修改” 状态
- 内置异步支持:无需额外配置 thunk
- 开发体验好:devtools 集成,序列化检查
- 官方推荐:是现在 Redux 的标准写法RTK 作用
RTK 核心 API
- configureStore 替代传统的 createStore,内置:redux-thunk、redux-devtools-extension、默认合并多个 reducer
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer
}
});- createSlice 自动生成 action creators 和 action types,内部使用 immer 让你 “直接修改” 状态。
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter', // slice 名称,会自动作为 action type 前缀
initialState: { value: 0 }, // 状态初始值
reducers: {
increment: state => {
state.value += 1; // 直接改,immer 会帮你转成不可变更新
},
decrement: state => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
}
}
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;- createAsyncThunk 方便处理异步逻辑(比如请求 API)。
import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
export const fetchUser = createAsyncThunk(
'users/fetchUser', // action type 前缀
async (userId, { rejectWithValue }) => {
try {
const response = await axios.get(`/api/users/${userId}`);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);- createSelector 用于创建记忆化的 selector,避免不必要的计算。
import { createSelector } from '@reduxjs/toolkit';
const selectCounter = state => state.counter;
export const selectCounterValue = createSelector(
[selectCounter],
counter => counter.value
);典型使用流程
- 创建 Slice(含 reducers / 异步逻辑)
- 用 configureStore 注册 reducer
- 在组件中使用 useSelector /useDispatch
import { useSelector, useDispatch } from 'react-redux';
import { increment } from './counterSlice';
function Counter() {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch(increment())}>+</button>
</div>
);
}异步流程示例
结合 createAsyncThunk 和 extraReducers
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import api from './api';
export const fetchPosts = createAsyncThunk(
'posts/fetchPosts',
async () => {
const response = await api.get('/posts');
return response.data;
}
);
const postsSlice = createSlice({
name: 'posts',
initialState: { items: [], status: 'idle', error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
}
});
export default postsSlice.reducer;基于 redux 的企业级状态管理
要点速览
# 综上 1- 11 可以发现,越往下状态管理越负责,概念多、杂、管理起来超级麻烦
所以当下流行的状态管理,都是基于 Toolkit 简化过的
## 方案一:中小型后台 / 中台 / 简单项目
选型:Redux Toolkit + react-redux
优点:最快、最简单、足够用。
## 方案二:大型商城 / 后台 / 复杂业务 / 高要求项目
选型:Redux Toolkit + Redux-Saga(最强组合)
优点如下:
- 极致可控
- 支持防抖、节流、取消、轮询、并发、重试、401 自动刷新 token
- 超大型项目唯一稳定方案
- 一线大厂后台系统主流架构Redux Toolkit(RTK)+ react-redux 方案
要点速览
✅ 完全模块化(user /auth/counter)
✅ 异步登录 + 错误处理 + loading
✅ localStorage 持久化(刷新不掉登录态)
✅ RTK 最佳实践 + 业务真实结构
✅ 纯函数组件 + useSelector + useDispatch
✅ 可直接用于公司项目案例集成
目录结构
src/
├── store/
│ ├── features/ # 业务模块(每个模块一个 slice)
│ │ ├── userSlice.js # 用户信息
│ │ ├── authSlice.js # 登录、token、持久化
│ │ └── counterSlice.js
│ └── index.js # 配置 store + 合并模块
│
├── App.js
└── main.js第一步:安装依赖
npm install @reduxjs/toolkit react-redux第二步:定义 user 切片
store/features/userSlice.js(用户信息)
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
username: '',
avatar: '',
role: ''
};
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUserInfo: (state, action) => {
state.username = action.payload.username;
state.avatar = action.payload.avatar;
state.role = action.payload.role;
},
clearUserInfo: (state) => {
state.username = '';
state.avatar = '';
state.role = '';
}
}
});
export const { setUserInfo, clearUserInfo } = userSlice.actions;
export default userSlice.reducer;第三步:定义 auth 切片
store/features/authSlice.js(登录 + 异步 + 持久化)
// 用户点击登录
// ↓
// dispatch(loginAsync({ username, password }))
// ↓
// [自动触发] loginAsync.pending → loading = true
// ↓
// 调用 loginApi(模拟网络请求)
// ↓
// 成功? → 是 → loginAsync.fulfilled → 保存数据 + isLogin = true
// ↓
// 否
// ↓
// loginAsync.rejected → error = '账号或密码错误'
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// 模拟登录接口
const loginApi = async (params) => {
await new Promise(r => setTimeout(r, 1000)); // 模拟网络延迟 1 秒
if (params.username === 'admin' && params.password === '123456') {
return {
token: 'TOKEN_2026_' + Math.random(),
username: 'admin',
avatar: 'https://avatars.githubusercontent.com/u/1',
role: 'admin'
};
}
throw new Error('账号或密码错误');
};
// 创建并导出 异步 thunk(核心异步逻辑)
export const loginAsync = createAsyncThunk(
'auth/loginAsync',
// action 类型前缀,会生成三个 action:
// auth/loginAsync/pending
// auth/loginAsync/fulfilled
// auth/loginAsync/rejected
async (params, { rejectWithValue }) => { // params: 登录表单数据
try {
const res = await loginApi(params); // 调用登录接口,登录后缓存数据
localStorage.setItem('token', res.token);
localStorage.setItem('userInfo', JSON.stringify(res));
return res; // 返回数据会作为 fulfilled action 的 payload
} catch (err) {
return rejectWithValue(err.message); // 返回错误信息作为 rejected action 的 payload
}
}
);
// 初始化本地存储
const loadPersist = () => {
const token = localStorage.getItem('token');
const userInfo = localStorage.getItem('userInfo');
return {
token: token || '',
userInfo: userInfo ? JSON.parse(userInfo) : null,
isLogin: !!token
};
};
const initialState = {
...loadPersist(),
loading: false,
error: ''
};
// 创建 Slice(同步 reducers)
const authSlice = createSlice({
name: 'auth', // slice 名称,action 会以 'auth/xxx' 格式生成
initialState,
reducers: { // 同步操作(不需要异步请求)
logoutSync: (state) => { // 退出登录
state.token = '';
state.userInfo = null;
state.isLogin = false;
localStorage.clear();
},
clearAuthError: (state) => { // 清除错误信息
state.error = '';
}
},
extraReducers: (builder) => { // 处理异步 thunk 的状态
builder
// 请求进行中
.addCase(loginAsync.pending, (state) => {
state.loading = true;
state.error = '';
})
// 请求成功
.addCase(loginAsync.fulfilled, (state, action) => {
state.loading = false;
state.token = action.payload.token;
state.userInfo = action.payload;
state.isLogin = true;
})
// 请求失败
.addCase(loginAsync.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
}
});
export const { logoutSync, clearAuthError } = authSlice.actions; // 导出同步 action
export default authSlice.reducer; // 导出 reducer(用于 store 配置)第四步:定义 counter 切片
store/features/counterSlice.js(演示模块)
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
add: (state) => { state.count += 1 },
minus: (state) => { state.count -= 1 }
}
});
export const { add, minus } = counterSlice.actions;
export default counterSlice.reducer;第五步:定义 store 合并所有模块
store/index.js
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './features/userSlice';
import authReducer from './features/authSlice';
import counterReducer from './features/counterSlice';
export const store = configureStore({
reducer: {
auth: authReducer, // 登录、token、持久化
user: userReducer, // 用户信息
counter: counterReducer // 测试模块
}
});第五步:入口配置
入口注入 main.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './store';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);第六步:在函数组件中
业务组件 App.js(真实登录页面)
import { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { loginAsync, logoutSync, clearAuthError } from './store/features/authSlice';
export default function App() {
const dispatch = useDispatch();
const { isLogin, userInfo, loading, error } = useSelector(state => state.auth);
const [form, setForm] = useState({ username: 'admin', password: '123456' });
const handleLogin = () => {
dispatch(clearAuthError());
dispatch(loginAsync(form));
};
return (
<div style={{ width: 400, margin: '80px auto' }}>
<h2>企业级模块化登录</h2>
{isLogin ? (
<div>
<h3>欢迎回来,{userInfo.username}</h3>
<p>角色:{userInfo.role}</p>
<button onClick={() => dispatch(logoutSync())}>退出登录</button>
</div>
) : (
<div>
<input
value={form.username}
onChange={e => setForm({ ...form, username: e.target.value })}
placeholder="用户名"
style={{ width: '100%', padding: 8, marginBottom: 10 }}
/>
<input
value={form.password}
onChange={e => setForm({ ...form, password: e.target.value })}
placeholder="密码"
type="password"
style={{ width: '100%', padding: 8 }}
/>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button
onClick={handleLogin}
disabled={loading}
style={{ width: '100%', padding: 10, marginTop: 10, background: '#1677ff', color: '#fff' }}
>
{loading ? '登录中...' : '登录'}
</button>
</div>
)}
</div>
);
}Redux Toolkit(RTK)+ react-redux 方案
要点速览
# 优势
模块化:按业务拆分,多人协作不冲突,易维护
异步可控:saga 处理复杂异步(防抖、取消、轮询、串行 / 并行请求)
数据持久化:刷新页面状态不丢失,支持白名单 / 黑名单 / 加密
TypeScript 友好:RTK 原生支持 TS,类型提示完善
生产可用:经过大型项目验证,性能、可维护性拉满
官方推荐:Redux 官方主推 RTK,生态成熟稳定
加密持久化数据
# 分工
RTK 负责简化状态定义
saga 负责复杂异步
redux-persist 负责持久化
模块化结构负责可维护性
# 理解
1. 在 Slice 切片中定义状态初始值、同步 Reducer、Action 创建函数,通过直接赋值修改 State(RTK 内置 Immer 自动处理不可变数据),统一导出 Reducer 和 Action 供外部使用。
2. 在 Saga 中处理所有异步逻辑(接口请求、延时、串行 / 并行、防抖等),通过监听 Action Type 触发异步任务,执行完成后通过 put 派发 Action 更新 State。
3. 在 rootSaga.js 中合并所有业务模块的 Saga;在 rootReducer.js 中合并所有业务模块的 Reducer,形成根 Reducer。
4. 在 store/index.js 中:创建 Store 实例 → 配置状态持久化 → 配置持久化数据加密 → 注入 Saga 中间件 → 启动根 Saga。
5. 在React 组件中,通过 useDispatch 派发 Slice 暴露的 Action,通过 useSelector 读取 Store 中的状态,实现状态获取与状态更新。
-->
1. Slice:定义状态、同步 reducer、action,支持直接赋值修改 state。
2. Saga:统一处理异步逻辑,监听 action,执行后派发 action 更新 state。
3. 根文件:rootSaga 合并所有 saga,rootReducer 合并所有 reducer。
4. Store:创建实例、配置持久化 + 加密、注入并启动 saga。
5. 组件:useSelector 获取状态,useDispatch 派发 action。
## 总结
在 slice 中管理状态和同步方法
在 saga 中监听 actionType 处理所有异步逻辑
相比 1 - 11,已简化很多案例集成
目录结构
src/
├── store/ # 状态管理根目录
│ ├── index.js # store 入口(创建、配置、持久化)
│ ├── rootSaga.js # saga 根入口
│ ├── rootReducer.js # reducer 根入口
│ └── modules/ # 业务模块(核心模块化拆分)
│ ├── user/ # 用户模块
│ │ ├── userSlice.js
│ │ └── userSaga.js
│ ├── app/ # 全局应用模块
│ │ ├── appSlice.js
│ │ └── appSaga.js
│ └── xxx/ # 其他业务模块...
├── api/ # 接口请求封装
└── App.js第一步:安装依赖
# 核心依赖
npm install @reduxjs/toolkit react-redux redux-saga redux-persist redux-persist-transform-encrypt第二步:封装 axios
src/api/request.js,企业级统一请求处理,saga 中直接调用
import axios from 'axios';
const request = axios.create({
baseURL: process.env.REACT_APP_API_URL,
timeout: 10000,
});
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 可添加 token
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器
request.interceptors.response.use(
(res) => res.data,
(error) => Promise.reject(error)
);
export default request;第三步:定义 user 切片
src/store/modules/user/userSlice.js(RTK 创建 reducer + action)
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
username: '',
avatar: '',
role: '',
token: '', // 新增:与组件使用状态匹配
nickname: '', // 新增:与组件使用状态匹配
loading: false // 新增:登录加载状态
};
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
// 触发登录异步请求
loginRequest: (state) => {
state.loading = true;
},
// 登录成功
loginSuccess: (state, action) => {
state.loading = false;
// 接口返回数据赋值
state.username = action.payload.username;
state.avatar = action.payload.avatar;
state.role = action.payload.role;
state.token = action.payload.token;
state.nickname = action.payload.nickname;
},
// 登录失败
loginFailure: (state) => {
state.loading = false;
},
// 清空用户信息
clearUserInfo: (state) => {
state.username = '';
state.avatar = '';
state.role = '';
state.token = '';
state.nickname = '';
}
}
});
// 导出所有 action
export const { loginRequest, loginSuccess, loginFailure, setUserInfo, clearUserInfo } = userSlice.actions;
export default userSlice.reducer;第四步:定义 user saga
src/store/modules/user/userSaga.js 异步逻辑
import { call, put, takeLatest } from 'redux-saga/effects';
import { loginRequest, loginSuccess, loginFailure } from './userSlice';
import request from '../../../api/request';
// 登录接口请求
function loginApi(userData) {
return request.post('/user/login', userData);
}
// 登录异步工作流 saga
function* loginSaga(action) {
try {
// 1. 调用接口
const res = yield call(loginApi, action.payload);
// 2. 触发成功 action,更新 state
yield put(loginSuccess(res.data));
} catch (error) {
// 3. 触发失败 action
yield put(loginFailure(error.message || '登录失败'));
}
}
// 监听 action,执行对应 saga(takeLatest:只执行最后一次请求,防抖)
export function* watchUserSaga() {
yield takeLatest(loginRequest.type, loginSaga);
}第五步:定义 app 切片
src/store/modules/app/appSlice.js (全局应用状态:主题、语言、全局 loading、菜单折叠等)
import { createSlice } from '@reduxjs/toolkit';
// 全局应用初始状态
const initialState = {
// 布局
sidebarCollapsed: false, // 侧边栏是否折叠
theme: 'light', // 主题 light / dark
language: 'zh-CN', // 语言
// 全局 loading(可用于页面加载、接口全局 loading)
globalLoading: false,
// 系统信息
systemConfig: null, // 后端返回的系统配置
};
const appSlice = createSlice({
name: 'app', // 模块唯一 key
initialState,
reducers: {
// ========== 同步 action ==========
toggleSidebar: (state) => {
state.sidebarCollapsed = !state.sidebarCollapsed;
},
setTheme: (state, action) => {
state.theme = action.payload;
},
setLanguage: (state, action) => {
state.language = action.payload;
},
setGlobalLoading: (state, action) => {
state.globalLoading = action.payload;
},
// ========== 触发 saga 异步 action ==========
fetchSystemConfigRequest: (state) => {
state.globalLoading = true; // 请求时开启 loading
},
fetchSystemConfigSuccess: (state, action) => {
state.systemConfig = action.payload;
state.globalLoading = false;
},
fetchSystemConfigFailure: (state) => {
state.globalLoading = false;
},
},
});
// 导出所有 action
export const {
toggleSidebar,
setTheme,
setLanguage,
setGlobalLoading,
fetchSystemConfigRequest,
fetchSystemConfigSuccess,
fetchSystemConfigFailure,
} = appSlice.actions;
export default appSlice.reducer;第六步:定义 app saga
src/store/modules/app/appSaga.js
import { call, put, takeLatest } from 'redux-saga/effects';
import {
fetchSystemConfigRequest,
fetchSystemConfigSuccess,
fetchSystemConfigFailure,
} from './appSlice';
import request from '../../../api/request';
// ========== 接口请求 ==========
// 获取系统全局配置
function fetchSystemConfigApi() {
return request.get('/system/config');
}
// ========== saga 工作流 ==========
function* fetchSystemConfigSaga() {
try {
// 调用接口
const res = yield call(fetchSystemConfigApi);
// 更新 state
yield put(fetchSystemConfigSuccess(res.data));
} catch (err) {
console.error('获取系统配置失败', err);
yield put(fetchSystemConfigFailure());
}
}
// ========== 监听 action ==========
export function* watchAppSaga() {
// 监听:获取系统配置(takeLatest 自动取消前面的重复请求)
yield takeLatest(fetchSystemConfigRequest.type, fetchSystemConfigSaga);
}第七步:合并所有模块 saga
store/rootSaga.js
import { all } from 'redux-saga/effects';
import { watchUserSaga } from './modules/user/userSaga';
import { watchAppSaga } from './modules/app/appSaga';
// 合并所有业务模块 saga
export default function* rootSaga() {
yield all([watchUserSaga(), watchAppSaga()]);
}第八步:合并所有模块 reducer
store/rootReducer.js
import { combineReducers } from '@reduxjs/toolkit';
import userReducer from './modules/user/userSlice';
import appReducer from './modules/app/appSlice';
// 模块化合并 reducer
const rootReducer = combineReducers({
user: userReducer, // user 模块 state:store.user
app: appReducer, // app 模块 state:store.app
// 其他模块...
});
export default rootReducer;第九步:创建 store + 持久化 + 注入 saga
store/index.js
import { configureStore } from '@reduxjs/toolkit';
import createSagaMiddleware from 'redux-saga';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage'; // 默认 localStorage
import rootReducer from './rootReducer';
import rootSaga from './rootSaga';
import { encryptTransform } from 'redux-persist-transform-encrypt';
// 1. 创建 saga 中间件
const sagaMiddleware = createSagaMiddleware();
// 2. 持久化配置(企业级可配置加密、白名单、黑名单)
const persistConfig = {
key: 'root', // 存储键名
storage, // 存储引擎
whitelist: ['user'], // 白名单:只持久化 user 模块
// blacklist: ['app'], // 黑名单:不持久化 app 模块(二选一)
transforms: [
encryptTransform({
secretKey: process.env.REACT_APP_PERSIST_KEY || 'default_secure_key',
onError: (error) => console.log('加密失败', error),
})
],
};
// 3. 创建持久化 reducer
const persistedReducer = persistReducer(persistConfig, rootReducer);
// 4. 创建 store
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false, // 关闭序列化检查(兼容 redux-persist)
}).concat(sagaMiddleware), // 注入 saga 中间件
});
// 5. 启动 saga
sagaMiddleware.run(rootSaga);
// 6. 创建持久化存储实例
export const persistor = persistStore(store);第十步:全局注入
在入口 main.js 中
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from './store';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
{/* 持久化加载屏障:等待存储恢复后再渲染页面 */}
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>
);第十一步:组件中使用
import { useDispatch, useSelector } from 'react-redux';
import { loginRequest } from './store/modules/user/userSlice';
function Login() {
const dispatch = useDispatch();
// 从 store 中获取状态(与 userSlice 完全匹配,无报错)
const { loading, username, nickname, token } = useSelector((state) => state.user);
const handleLogin = () => {
// 触发 saga 异步登录
dispatch(loginRequest({ username: 'admin', password: '123456' }));
};
return (
<div>
<button onClick={handleLogin} disabled={loading}>
{loading ? '登录中...' : '登录'}
</button>
{nickname && <div>欢迎:{nickname}</div>}
</div>
);
}
export default Login;