Skip to content

React 文档

导读与全书目录表:见同目录 01-React 入门:简介·特性与设计范式.md 开头。


说说你对 redux 的理解

回答

shell
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 核心与使用场景

shell
Redux 是基于 Flux 架构的状态管理库,核心是 “单一数据源、状态只读、纯函数修改”,由 Store(存储状态)、Action(描述状态变更)、Reducer(纯函数,根据 Action 更新状态)组成。它适合跨组件、多层级共享状态的场景,比如用户登录状态、全局主题配置。实际项目中,我会结合 Redux Toolkit(官方推荐)简化 Redux 代码,避免手写重复的 action reducer,同时用 useSelector useDispatch Hook 在组件中获取 / 修改状态,替代传统的 connect 高阶组件。

状态管理

要点速览

shell
# 主流方案对比
特性      Zustand         Redux                   Context + useState
代码量    极少             多(action/reducer)     中(Provider 包裹)
学习成本  极低
重渲染    细粒度(精准订阅) 需手动优化                全量渲染(性能差)
异步处理  原生支持                       redux-thunk/saga 原生支持
适用场景  中小型、快速开发   大型复杂项目               简单共享、低频更新

Zustand 状态管理

Zustand 是 React 生态里轻量、无 Provider、极简 API 的状态管理库,主打 “用 Hook 管理全局状态”,替代 Context / Redux,兼顾开发效率与性能。

要点速览

shell
# 特点
德语 “状态” 之意,1KB 左右、零依赖。

# 核心优势(对比 Context/Redux):
 Provider:不用包裹根组件,直接 import store 使用。
 极简 API:仅 create 一个核心函数,无 action/reducer 模板代码。
 细粒度更新:组件只订阅需要的字段,避免 Context 全量渲染。
 TS 友好:天然支持类型推导,无需额外配置。
 中间件丰富:持久化、DevTools、日志、undo 等。

实战

js
// 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>
    </>
  )
}

性能关键

js
// 只取需要的状态,精准订阅、最小化重渲染:
// ✅ 推荐:仅 bears 变化时更新
const bears = useBearStore(state => state.bears)

// ❌ 不推荐:状态任何变化都更新
const state = useBearStore(state => state)

持久化

持久化(persist 中间件):数据存 localStorage

js
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
  )
)

调试

js
import { devtools } from 'zustand/middleware'
export const useStore = create(devtools((set) => ({...})))

模块化目录

js
stores/
├── useUserStore.js
├── useCartStore.js
└── useTabStore.js

TS 支持

自动推导类型,无需手动写接口

js
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 的状态管理库,主要用于管理应用中共享状态

简单

shell
# 三个核心原则
- 其一:单一状态树,所有数据都存储在唯一一个 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 结构
要配中间件
要模块化合并
代码量巨大

案例

目录

shell
src/
├── store/
   ├── index.js          # store 配置文件
   └── reducers.js       # reducer 文件
└── components/
    └── UserProfile.js    # 上面的类组件

第一步:定义 store 和 reducer

1. 在 store/index.js 中定义 store 模块化

js
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

js
// 初始状态
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

js
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

js
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() 等方法创建新的状态副本:

js
// Reducer 必须是纯函数,不能包含 API 调用、定时器、日志打印等副作用,这些操作应放在 action 创建函数或中间件中。
// 错误做法:直接修改状态
state.todos.push(newTodo);

// 正确做法:返回新状态
return {
  ...state,
  todos: [...state.todos, newTodo]
};

React-Redux 是什么

react-redux 是连接 Redux 与 React 的官方绑定库(react-redux 包),让组件订阅 store、派发 action 更便捷

要点速览

shell
# 为何使用 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 方法派发 action

React-Redux 示例

shell
# 安装依赖
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 中的异步操作。

要点速览

shell
# 为什么 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 中

shell
# 安装依赖
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 函数 让异步流程更易读、易测试。

要点速览

shell
# 什么是 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

shell
# 基础 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):启动根 saga

Effects API 示例

1. take / takeEvery / takeLatest

js
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

js
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

js
import { put } from 'redux-saga/effects';

// 相当于 dispatch
yield put({ type: 'ACTION_TYPE', payload: data });

// 批量 dispatch
yield put.all([
  put(action1()),
  put(action2())
]);

4. select

js
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

js
import { fork, spawn } from 'redux-saga/effects';

// fork:非阻塞执行任务
function* mainSaga() {
  yield fork(watchFetchUser);  // 不等待,继续执行
  yield fork(watchUpdateUser);
}

// spawn:类似 fork,但任务独立于父级(错误不会影响父级)
yield spawn(backgroundTask);

6. cancel

js
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

js
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

js
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 基础案例

要点速览

shell
# 相比于 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 依旧是同步写法

shell
# 安装依赖
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

要点速览

shell
在组件中只需要派发最后一个 take action 即可,执行异步中的所有任务

前置

先统一:actionTypes

js
// 基础
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、延迟异步(最基础)

js
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、请求接口(最常用)

js
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、防抖(只执行最后一次)搜索必备

js
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、节流(只执行第一次)防重复点击

js
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、轮询(自动刷新)订单 / 消息 / 数据

js
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、取消异步任务

js
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、并发请求(同时发多个接口)

js
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

js
import { takeEvery } from 'redux-saga/effects';

function* handleMulti() {
  console.log('触发了多个 action 中的一个');
}

function* watchMulti() {
  yield takeEvery(['ACTION1', 'ACTION2', 'ACTION3'], handleMulti); // 1. 每次匹配都执行,允许并发
}

最终完整 rootSaga

js
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 企业案例

要点速览

shell

案例

第一步:安装依赖

npm i redux react-redux redux-saga

第二步:统一 actionTypes.js

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

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(所有案例集成)

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

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)

js
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 中

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

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 根组件中

js
import { Provider } from 'react-redux';
import store from './store';

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

redux-saga 架构目录

项目目录

js
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 原理

要点速览

shell
# 底层原理
基于 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 模块化

要点速览

shell
# 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

案例

目录结构

shell
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           # 创建 store

1、业务-计数 reducer

store/reducers/counterReducer.js

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

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

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

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

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 全部合并

js
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)

js
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) 是什么

要点速览

shell
# 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

  1. configureStore 替代传统的 createStore,内置:redux-thunk、redux-devtools-extension、默认合并多个 reducer
js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer
  }
});
  1. createSlice 自动生成 action creators 和 action types,内部使用 immer 让你 “直接修改” 状态。
js
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;
  1. createAsyncThunk 方便处理异步逻辑(比如请求 API)。
js
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);
    }
  }
);
  1. createSelector 用于创建记忆化的 selector,避免不必要的计算。
js
import { createSelector } from '@reduxjs/toolkit';

const selectCounter = state => state.counter;
export const selectCounterValue = createSelector(
  [selectCounter],
  counter => counter.value
);

典型使用流程

  1. 创建 Slice(含 reducers / 异步逻辑)
  2. 用 configureStore 注册 reducer
  3. 在组件中使用 useSelector /useDispatch
js
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

js
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 的企业级状态管理

要点速览

shell
# 综上 1- 11 可以发现,越往下状态管理越负责,概念多、杂、管理起来超级麻烦
所以当下流行的状态管理,都是基于 Toolkit 简化过的

## 方案一:中小型后台 / 中台 / 简单项目
选型:Redux Toolkit + react-redux
优点:最快、最简单、足够用。

## 方案二:大型商城 / 后台 / 复杂业务 / 高要求项目
选型:Redux Toolkit + Redux-Saga(最强组合)
优点如下:
- 极致可控
- 支持防抖、节流、取消、轮询、并发、重试、401 自动刷新 token
- 超大型项目唯一稳定方案
- 一线大厂后台系统主流架构

Redux Toolkit(RTK)+ react-redux 方案

要点速览

shell
 完全模块化(user /auth/counter)
 异步登录 + 错误处理 + loading
 localStorage 持久化(刷新不掉登录态)
 RTK 最佳实践 + 业务真实结构
 纯函数组件 + useSelector + useDispatch
 可直接用于公司项目

案例集成

目录结构

shell
src/
├── store/
   ├── features/         # 业务模块(每个模块一个 slice)
   ├── userSlice.js  # 用户信息
   ├── authSlice.js  # 登录、token、持久化
   └── counterSlice.js
   └── index.js          # 配置 store + 合并模块

├── App.js
└── main.js

第一步:安装依赖

shell
npm install @reduxjs/toolkit react-redux

第二步:定义 user 切片

store/features/userSlice.js(用户信息)

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(登录 + 异步 + 持久化)

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(演示模块)

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

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

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(真实登录页面)

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 方案

要点速览

shell
# 优势
模块化:按业务拆分,多人协作不冲突,易维护
异步可控: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,已简化很多

案例集成

目录结构

shell
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

第一步:安装依赖

shell
# 核心依赖
npm install @reduxjs/toolkit react-redux redux-saga redux-persist redux-persist-transform-encrypt

第二步:封装 axios

src/api/request.js,企业级统一请求处理,saga 中直接调用

js
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)

js
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 异步逻辑

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、菜单折叠等)

js
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

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

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

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

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 中

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>
);

第十一步:组件中使用

js
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;