React 状态管理与网络请求实战笔记:Zustand + Axios 深度解析

5 阅读18分钟

React 状态管理与网络请求实战笔记:Zustand + Axios 深度解析

在 React 项目开发中,状态管理与网络请求是两大核心模块。 Zustand 作为轻量级状态管理库,以简洁的 API、良好的 TypeScript 支持的优势,逐渐成为 Redux、Context API 的替代方案之一;而 Axios 则是前端网络请求的主流工具,通过拦截器可实现请求统一处理、响应规范化等功能。本文结合实际项目代码,深入剖析 Zustand 状态管理、Axios 拦截器的实战用法,重点解决两者结合使用中的常见问题(如 Hook 调用错误),并梳理核心知识点与最佳实践,助力开发者高效搭建稳定的 React 应用。

一、技术栈核心概述

1.1 Zustand 简介

Zustand 是由 Poimandres(原 React-Spring 团队)开发的轻量级 React 状态管理库,核心特点的是“简洁、高效、无冗余模板”。与 Redux 繁琐的 Action、Reducer、中间件配置不同,Zustand 基于 Hook 设计,无需 Provider 包裹组件树,可直接在组件中通过自定义 Hook 访问和修改状态;同时原生支持 TypeScript,类型推导清晰,能有效避免类型错误。

适用于中小型项目的全局状态管理(如用户信息、权限控制、页面公共状态),也可与局部状态配合使用,平衡开发效率与性能。本文中主要用于管理用户登录状态(token、用户信息)和首页数据状态(轮播图、帖子列表)。

1.2 Axios 简介

Axios 是一款基于 Promise 的 HTTP 客户端,支持浏览器和 Node.js 环境,提供了请求/响应拦截器、请求取消、超时设置、JSON 自动转换等强大功能。在 React 项目中,Axios 常被用于统一管理 API 请求,通过拦截器实现请求头添加(如 Token 认证)、响应数据格式化、错误统一处理等需求,减少重复代码,提升项目可维护性。

1.3 技术结合场景

本文项目中,Zustand 与 Axios 的核心结合点在于:通过 Zustand 存储用户登录后的 Token 信息,Axios 请求拦截器从 Zustand 中获取 Token 并添加到请求头,实现接口的身份认证;同时,Zustand 管理首页帖子列表的加载状态与数据,通过异步方法调用 Axios 请求接口,完成数据获取与状态更新。这一组合覆盖了“状态存储-网络请求-状态同步”的完整业务链路,是 React 项目的典型应用场景。

二、Zustand 状态管理实战

2.1 Zustand 核心 API 解析

Zustand 的 API 极简,核心仅包含 create 方法(创建状态容器),配合中间件(如 persist 持久化存储)可扩展功能。以下结合项目代码拆解核心用法:

2.1.1 create 方法:创建状态容器

create 是 Zustand 的核心方法,用于创建一个状态容器,接收一个函数作为参数,该函数返回状态对象与修改状态的方法(通过 set 函数更新状态)。同时支持 TypeScript 泛型,可定义状态接口,实现类型约束。

语法格式:

import { create } from 'zustand';

interface State {
  // 状态字段
  count: number;
  // 状态修改方法
  increment: () => void;
}

// 创建状态容器
export const useCountStore = create<State>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

其中,set 函数有两种用法:

  • 直接传入状态对象:set({ count: 1 }),适用于无需依赖当前状态的场景;
  • 传入函数:set((state) => ({ count: state.count + 1 })),适用于需要基于当前状态计算新状态的场景,确保状态更新的准确性。

2.1.2 persist 中间件:状态持久化

默认情况下,Zustand 管理的状态存储在内存中,页面刷新后会丢失。persist 是 Zustand 官方提供的中间件,可将状态持久化到 localStoragesessionStorage,页面刷新后自动恢复状态,适用于用户信息、登录状态等需要长期保存的数据。

核心配置参数:

  • name:持久化存储的键名,用于在存储中标识当前状态;
  • partialize:可选参数,用于指定需要持久化的状态字段,避免不必要的数据存储;
  • storage:可选参数,指定存储方式(默认 localStorage,可改为 sessionStorage)。

2.2 项目状态管理实现

本文项目中,基于 Zustand 实现了两个核心状态容器:useUserStore(用户状态管理)和 useHomeStore(首页数据状态管理),分别对应用户登录模块和首页展示模块。

2.2.1 用户状态管理:useUserStore

该状态容器用于管理用户的登录状态(token、用户信息、是否登录),并提供登录方法,结合 persist 中间件实现状态持久化。以下是代码拆解:

// 导入依赖
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { doLogin } from '@/api/user';
import type { User } from '@/types';
import type { Credential } from '@/types';

// 定义状态接口,约束状态字段与方法类型
interface UserStore {
  token: string; // 登录凭证 Token
  user: User | null; // 用户信息,联合类型(存在/不存在)
  isLogin: boolean; // 是否登录标识
  login: (credentials: Credential) => Promise<void>; // 登录方法,异步无返回值
}

// 创建用户状态容器,结合 persist 中间件实现持久化
export const useUserStore = create<UserStore>()(
  persist(
    (set) => ({
      // 初始状态
      token: "",
      user: null,
      isLogin: false,
      // 登录方法:异步请求接口,更新状态
      login: async ({ name, password }) => {
        const res = await doLogin({ name, password }); // 调用登录 API
        console.log(res, '登录结果');
        // 通过 set 函数更新状态
        set({
          user: res.user,
          token: res.token,
          isLogin: true,
        });
      }
    }),
    {
      name: 'user-store', // 持久化存储的键名
      // 指定需要持久化的字段,避免冗余数据
      partialize: (state) => ({
        token: state.token,
        user: state.user,
        isLogin: state.isLogin,
      })
    }
  )
);
关键知识点解析:
  1. 类型约束:通过 UserStore 接口明确状态字段的类型,login 方法接收 Credential 类型参数(包含用户名、密码),返回 Promise,确保 TypeScript 类型推导准确,减少运行时错误。
  2. 异步状态更新:登录方法为异步函数,通过 await doLogin 等待登录接口响应,获取 Token 和用户信息后,通过 set 函数更新状态。Zustand 支持异步方法直接写入状态容器,无需额外处理中间件(如 Redux-Thunk),简化异步逻辑。
  3. 状态持久化persist 中间件包裹状态容器后,指定 name: 'user-store',状态会自动存储到 localStorage.getItem('user-store') 中;partialize 配置仅持久化 tokenuserisLogin 三个字段,避免存储无关数据,优化性能。

2.2.2 首页状态管理:useHomeStore

该状态容器用于管理首页的轮播图数据、帖子列表数据,以及帖子加载方法,实现首页数据的统一管理。代码拆解如下:

import { create } from "zustand";
import type { SlideData } from "@/components/SlideShow";
import type { Post } from "@/types";
import { fetchPosts } from "@/api/posts";

// 定义首页状态接口
interface HomeState {
  banners: SlideData[]; // 轮播图数据
  posts: Post[]; // 帖子列表数据
  loadMore: () => Promise<void>; // 加载更多帖子方法
}

// 创建首页状态容器
export const useHomeStore = create<HomeState>((set) => ({
  // 初始轮播图数据
  banners: [
    {
      id: 1,
      title: "React 生态系统",
      image: "https://images.unsplash.com/photo-1633356122544-f134324a6cee?q=80&w=2070&auto=format&fit=crop",
    },
    {
      id: 2,
      title: "移动端开发最佳实践",
      image: "https://img.36krcdn.com/hsossms/20260114/v2_1ddcc36679304d3390dd9b8545eaa57f@5091053@ai_oswg1012730oswg1053oswg495_img_png~tplv-1marlgjv7f-ai-v3:600:400:600:400:q70.jpg?x-oss-process=image/format,webp",
    },
    {
      id: 3,
      title: "百度上线七猫漫剧,打的什么主意?",
      image: "https://img.36krcdn.com/hsossms/20260114/v2_8dc528b02ded4f73b29b7c1019f8963a@5091053@ai_oswg1137571oswg1053oswg495_img_png~tplv-1marlgjv7f-ai-v3:600:400:600:400:q70.jpg?x-oss-process=image/format,webp",
    }
  ],
  posts: [], // 初始帖子列表为空
  // 加载更多帖子方法
  loadMore: async () => {
    console.log(await fetchPosts()); // 调用获取帖子 API
  }
}));
关键知识点解析:
  1. 静态数据初始化:轮播图 banners 字段初始化为固定数组,适用于无需接口请求的静态数据;帖子列表 posts 初始化为空数组,等待接口请求后填充数据。
  2. 状态与 API 结合loadMore 方法为异步函数,调用 fetchPosts 接口获取帖子数据,后续可扩展为“获取数据后更新 posts 状态”(如合并新旧数据),实现无限滚动加载功能。
  3. 可扩展性:当前代码为基础骨架,可扩展添加 page(当前页码)、isLoading(加载中状态)、hasMore(是否有更多数据)等字段,优化加载逻辑(如防止重复请求、显示加载提示)。

2.3 Zustand 核心踩坑点与解决方案

在使用 Zustand 过程中,最常见的问题是“Hook 调用错误”,尤其是在非 React 组件环境中调用 Zustand 自定义 Hook。以下结合项目报错场景,深入分析原因与解决方案。

2.3.1 核心问题:Hook 执行环境限制

Zustand 提供的 useUserStoreuseHomeStore 本质是 React 自定义 Hook,遵循 React Hooks 的核心规则:只能在 React 函数组件或自定义 Hook 的顶层执行,不能在普通函数、if/for 循环、异步代码、Axios 拦截器中调用

项目中曾出现如下报错:

Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
    at Object.throwInvalidHookError (react-dom_client.js)
    at exports.useCallback (chunk-EU35WYTC.js)
    at useStore (zustand.js)
    at useBoundStore (zustand.js)
    at config.ts:10:19
    at async Axios.request (axios.js)

报错根源:在 Axios 请求拦截器(普通 JavaScript 函数,脱离 React 组件渲染流程)中直接调用 useUserStore() 自定义 Hook,违反了 Hooks 使用规则。

2.3.2 解决方案:getState() 方法

Zustand 为非 React 环境(如普通函数、拦截器、工具函数)提供了 getState() 方法,该方法并非 Hook,无 React 环境限制,可在任意位置调用,用于获取当前状态的快照。

核心特点:
  1. 无环境限制:不属于 React Hook,可在 Axios 拦截器、普通工具函数、异步代码中自由调用;
  2. 实时获取状态:每次调用都会获取最新的状态值,适用于 Token 刷新、状态动态变化的场景;
  3. 同步执行:无需异步等待,不影响原有代码流程(如 Axios 拦截器的执行效率)。

项目中修正后的 Axios 拦截器代码如下(后续章节详细解析):

// 在 Axios 请求拦截器中使用 getState() 获取 Token
axios.interceptors.request.use(config => {
  const token = useUserStore.getState().token; // 非 Hook 调用,无环境限制
  if(token){
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config
});

2.3.3 补充说明:getState() 与 Hook 调用的区别

调用方式适用场景是否响应式环境限制
useUserStore()(Hook 调用)React 函数组件/自定义 Hook是,状态变化时组件自动重渲染有,仅能在组件顶层调用
useUserStore.getState()(非 Hook 调用)普通函数、拦截器、工具函数否,仅获取当前快照,需手动监听变化无,可在任意位置调用

总结:在 React 组件中,优先使用 Hook 调用方式(响应式更新);在非 React 环境中,使用 getState() 方法获取状态。

三、Axios 网络请求实战

3.1 Axios 基础配置

项目中通过单独的 config.ts 文件对 Axios 进行全局配置,包括基础路径、请求拦截器、响应拦截器,实现请求/响应的统一处理。基础配置代码如下:

import axios from 'axios';
import { useUserStore } from '@/store/useUserStore';

// 配置基础路径(区分开发环境:Mock 服务/后端服务)
axios.defaults.baseURL = 'http://localhost:5173/api'; // 前端 Mock 地址
// axios.defaults.baseURL = 'http://localhost:3000/api'; // 后端服务地址

// 请求拦截器:请求发送前处理
axios.interceptors.request.use(config => {
  // 从 Zustand 获取 Token,添加到请求头
  const token = useUserStore.getState().token;
  if(token){
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config
});

// 响应拦截器:响应接收后处理
axios.interceptors.response.use(res => {
  console.log('////');
  if(res.status != 200){
    console.log(res,'响应错误');
    return;
  }
  return res.data; // 统一返回响应数据,简化组件中数据获取逻辑
})

export default axios;

3.2 拦截器核心功能解析

3.2.1 请求拦截器

请求拦截器通过 axios.interceptors.request.use() 注册,在请求发送到服务器之前执行,核心作用包括:

  1. 添加请求头:如添加 Token 认证信息(Authorization 字段)、设置 Content-Type(如 JSON 格式);
  2. 请求参数格式化:如统一处理日期格式、参数序列化;
  3. 添加公共参数:如所有请求携带用户 ID、设备信息等;
  4. 请求加载状态控制:如设置全局加载中状态(显示 Loading 组件)。

项目中请求拦截器的核心逻辑是“从 Zustand 获取 Token 并添加到请求头”,实现接口的身份认证。需要注意的是,此处必须使用 getState() 方法而非 Hook 调用,避免触发无效 Hook 错误。

3.2.2 响应拦截器

响应拦截器通过 axios.interceptors.response.use() 注册,在组件接收响应数据之前执行,核心作用包括:

  1. 响应数据规范化:如统一返回 res.data,避免组件中重复书写 response.data
  2. 错误统一处理:如拦截 401(未授权)、403(权限不足)、500(服务器错误)等状态码,执行对应逻辑(如 401 跳转登录页);
  3. 加载状态关闭:如请求完成后关闭全局 Loading 组件;
  4. 数据转换:如将后端返回的时间戳转换为日期格式。

项目中响应拦截器的逻辑较为基础:判断响应状态码是否为 200,非 200 时打印错误信息;200 时返回 res.data,简化组件中数据处理流程。可扩展的优化点:

// 优化后的响应拦截器
axios.interceptors.response.use(
  res => {
    return res.data;
  },
  (error) => {
    // 错误统一处理
    const status = error.response?.status;
    switch(status){
      case 401:
        // Token 过期或未登录,跳转登录页
        window.location.href = '/login';
        break;
      case 403:
        // 权限不足,提示用户
        alert('无权限访问该资源');
        break;
      case 500:
        // 服务器错误,提示用户
        alert('服务器异常,请稍后重试');
        break;
      default:
        console.error('请求错误:', error.message);
    }
    return Promise.reject(error);
  }
);

3.3 API 接口封装

项目中通过单独的 API 文件夹封装接口请求(如 posts.ts),将网络请求逻辑与组件分离,提升代码可维护性。以获取帖子列表接口为例,代码如下:

import axios from './config';
import type { Post } from '@/types';

/**
 * 获取帖子列表
 * @param page 页码,默认1
 * @param limit 每页条数,默认10
 * @returns 帖子列表数据
 */
export const fetchPosts = async (
  page: number = 1,
  limit: number = 10,
) => {
  try{
    const response = await axios.get('/posts',{
      params:{
        page,
        limit,
      }
    })
    console.log('获取帖子列表成功',response);
    return response;
  }catch(error){
    console.error('获取帖子列表失败',error);
    throw error; // 抛出错误,让调用方处理
  }
};
接口封装核心原则:
  1. 类型约束:通过 TypeScript 定义参数类型(pagelimit)和返回值类型(Post 数组),确保类型安全;
  2. 参数默认值:为页码、每页条数设置默认值(page=1limit=10),简化调用;
  3. 错误处理:使用 try/catch 捕获请求错误,打印错误信息并抛出错误,让调用方(如组件、状态方法)根据业务需求处理错误(如显示错误提示);
  4. 职责单一:每个函数仅对应一个 API 接口,避免逻辑冗余,便于后续维护和修改。

四、Zustand 与 Axios 结合实战:完整业务链路

本节结合项目代码,梳理“用户登录-获取 Token-请求帖子列表-更新状态”的完整业务链路,展示 Zustand 与 Axios 的协同工作流程,同时优化首页加载更多逻辑。

4.1 完整业务流程拆解

  1. 用户登录:用户在登录页输入用户名和密码,调用useUserStore.login() 方法,该方法通过 Axios 调用登录接口(doLogin),获取 Token 和用户信息后,更新 Zustand 状态并持久化到 localStorage
  2. 请求帖子列表:用户进入首页,调用 useHomeStore.loadMore() 方法,该方法调用 fetchPosts 接口;
  3. 请求拦截器处理:Axios 请求拦截器通过 useUserStore.getState().token 获取 Token,添加到请求头Authorization 中,发送请求到服务器;
  4. 响应拦截器处理:服务器验证 Token 有效后,返回帖子列表数据,响应拦截器提取 res.data 并返回;
  5. 状态更新loadMore 方法获取帖子数据后,通过 set 函数更新 useHomeStore 中的 posts 状态,首页组件监听状态变化并重新渲染,展示帖子列表。

4.2 优化首页加载更多逻辑

当前 useHomeStore 中的 loadMore 方法仅调用接口,未更新状态。以下优化代码,实现“加载更多-合并数据-防止重复请求”的完整逻辑:

import { create } from "zustand";
import type { SlideData } from "@/components/SlideShow";
import type { Post } from "@/types";
import { fetchPosts } from "@/api/posts";

interface HomeState {
  banners: SlideData[];
  posts: Post[];
  page: number; // 当前页码
  isLoading: boolean; // 加载中状态
  hasMore: boolean; // 是否有更多数据
  loadMore: () => Promise<void>;
}

export const useHomeStore = create<HomeState>((set, get) => ({
  banners: [
    // 轮播图数据(略)
  ],
  posts: [],
  page: 1,
  isLoading: false,
  hasMore: true,
  loadMore: async () => {
    const { page, isLoading, hasMore } = get(); // 获取当前状态
    // 防止重复请求、无更多数据时停止请求
    if (isLoading || !hasMore) return;
    set({ isLoading: true }); // 设置加载中状态
    try {
      const data = await fetchPosts(page, 10); // 调用接口,传入页码和条数
      // 判断是否有更多数据(假设后端返回 total 字段)
      const newHasMore = data.total > page * 10;
      // 合并新旧帖子数据,更新状态
      set({
        posts: prev => [...prev, ...data.list], // 合并数组,保留原有数据
        page: page + 1, // 页码自增
        hasMore: newHasMore,
        isLoading: false, // 关闭加载中状态
      });
    } catch (error) {
      console.error('加载更多帖子失败', error);
      set({ isLoading: false }); // 加载失败也关闭加载中状态
    }
  }
}));
优化点解析:
  1. 添加状态字段:新增 page(当前页码)、isLoading(加载中状态)、hasMore(是否有更多数据),完善加载逻辑;
  2. 防止重复请求:通过 if (isLoading || !hasMore) return 阻止加载中或无更多数据时的重复请求;
  3. 状态联动更新:使用 get() 方法获取当前状态(页码、加载状态),请求成功后合并新旧帖子数据([...prev, ...data.list]),更新页码和是否有更多数据的状态;
  4. 异常处理优化:加载失败时关闭加载中状态,避免 Loading 组件一直显示,提升用户体验。

4.3 组件中使用状态与接口

在首页组件中,通过 Zustand Hook 访问状态和方法,实现数据展示与加载更多功能:

import { useHomeStore } from '@/store/useHomeStore';
import SlideShow from '@/components/SlideShow';

const Home = () => {
  // 从 Zustand 获取状态和方法(Hook 调用,响应式更新)
  const { banners, posts, isLoading, hasMore, loadMore } = useHomeStore();

  // 滚动到底部触发加载更多
  const handleScroll = () => {
    const scrollTop = document.documentElement.scrollTop;
    const scrollHeight = document.documentElement.scrollHeight;
    const clientHeight = document.documentElement.clientHeight;
    if (scrollTop + clientHeight >= scrollHeight - 100 && !isLoading && hasMore) {
      loadMore(); // 触发加载更多
    }
  };

  return (
    <div onScroll={handleScroll} style={{ height: '100vh', overflow: 'auto' }}>
      {/* 轮播图组件 */}
      <SlideShow banners={banners} />
      {/* 帖子列表 */}
      <div className="post-list">
        {posts.map(post => (
          <div key={post.id} className="post-item">
            <h3>{post.title}</h3>
            <p>{post.content}</p>
          </div>
        ))}
      </div>
      {/* 加载提示 */}
      {isLoading && <div className="loading">加载中...</div>}
      {/* 无更多数据提示 */}
      {!hasMore && !isLoading && <div className="no-more">没有更多帖子了</div>}
    </div>
  );
};

export default Home;

组件中通过 useHomeStore() Hook 获取状态和方法,当 postsisLoading 等状态变化时,组件会自动重渲染,展示最新数据;同时监听滚动事件,滚动到底部时触发 loadMore 方法,实现无限滚动加载。

五、常见问题与进阶优化

5.1 常见问题汇总与解决方案

5.1.1 问题1:Token 过期导致请求失败

现象:用户长时间未操作,Token 过期,调用接口时返回 401 状态码。

解决方案:在 Axios 响应拦截器中拦截 401 状态码,执行 Token 刷新逻辑或跳转登录页:

// 响应拦截器中添加 401 处理
axios.interceptors.response.use(
  res => res.data,
  async (error) => {
    const status = error.response?.status;
    if (status === 401) {
      const store = useUserStore.getState();
      // 若有刷新 Token 的接口,可在此调用
      // const newToken = await refreshToken(store.refreshToken);
      // 刷新成功后更新 Token
      // useUserStore.setState({ token: newToken });
      // 重试原请求
      // return axios(error.config);
      // 无刷新接口时,跳转登录页,清除状态
      store.setState({ token: '', user: null, isLogin: false });
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

5.1.2 问题2:Zustand 状态持久化后,TypeScript 类型报错

现象:使用 persist 中间件后,TypeScript 提示状态类型不匹配。

解决方案:确保 partialize 配置中指定的字段与状态接口一致,且初始状态类型正确。若仍报错,可通过泛型明确持久化类型:

import { create } from 'zustand';
import { persist, PersistMiddleware } from 'zustand/middleware';

// 明确持久化中间件的类型
type UserPersistMiddleware = PersistMiddleware<UserStore, UserStore>;

export const useUserStore = create<UserStore>()(
  persist<UserStore, UserPersistMiddleware>(
    (set) => ({ /* 状态定义 */ }),
    { name: 'user-store' }
  )
);

5.1.3 问题3:Axios 重复请求导致数据错乱

现象:快速滚动到底部时,多次触发loadMore 方法,导致多个请求同时发送,返回数据顺序错乱。

解决方案:通过 isLoading 状态控制,加载中时禁止重复请求(已在 4.2 节优化中实现),同时可使用 Axios 取消请求功能,取消前一个未完成的请求:

// 在 useHomeStore 中添加取消请求的控制器
import { CancelTokenSource } from 'axios';

interface HomeState {
  // 其他状态...
  cancelSource: CancelTokenSource | null;
}

export const useHomeStore = create<HomeState>((set, get) => ({
  // 其他初始状态...
  cancelSource: null,
  loadMore: async () => {
    const { page, isLoading, hasMore, cancelSource } = get();
    if (isLoading || !hasMore) return;
    // 取消前一个未完成的请求
    if (cancelSource) cancelSource.cancel('请求被取消');
    // 创建新的取消控制器
    const newCancelSource = axios.CancelToken.source();
    set({ isLoading: true, cancelSource: newCancelSource });
    try {
      const data = await fetchPosts(page, 10, newCancelSource);
      // 状态更新逻辑(略)
    } catch (error) {
      if (axios.isCancel(error)) {
        console.log('请求被取消:', error.message);
      } else {
        console.error('加载失败:', error);
      }
    } finally {
      set({ isLoading: false });
    }
  }
}));

5.2 进阶优化建议

5.2.1 Zustand 状态分片与选择器

当状态容器较大时,可通过选择器(Selector)仅订阅需要的状态字段,避免组件不必要的重渲染:

// 组件中仅订阅 posts 和 isLoading 字段
const { posts, isLoading } = useHomeStore(
  (state) => ({ posts: state.posts, isLoading: state.isLoading })
);

同时,可将大型状态容器拆分为多个小容器(如用户状态、首页状态、设置状态),提升代码可读性和维护性。

5.2.2 Axios 请求缓存

对于不常变化的数据(如轮播图、分类列表),可添加请求缓存功能,减少重复请求,提升性能。可使用 axios-cache-adapter 插件,或手动实现缓存逻辑:

// 手动实现简单缓存
const cache = new Map();

export const fetchBanners = async () => {
  // 检查缓存,存在则直接返回
  if (cache.has('banners')) {
    return cache.get('banners');
  }
  const data = await axios.get('/banners');
  cache.set('banners', data); // 存入缓存
  return data;
};

5.2.3 环境变量区分基础路径

项目开发中,需区分开发环境、测试环境、生产环境的 API 基础路径,可通过环境变量实现:

// 根据环境变量设置基础路径
if (import.meta.env.MODE === 'development') {
  axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL_DEV;
} else if (import.meta.env.MODE === 'production') {
  axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL_PROD;
}

.env.development.env.production 文件中配置对应的环境变量,避免手动修改代码。

六、总结与展望

6.1 核心知识点总结

本文围绕 Zustand 状态管理与 Axios 网络请求,结合项目实战代码,梳理了以下核心知识点:

  1. Zustand 用法:通过 create 方法创建状态容器,persist 中间件实现状态持久化,getState() 方法在非 React 环境中获取状态,遵循 React Hooks 规则避免调用错误;
  2. Axios 配置:通过请求/响应拦截器实现 Token 添加、错误统一处理、数据规范化,接口封装遵循类型约束和职责单一原则;
  3. 两者结合:Zustand 存储全局状态(Token、用户信息、页面数据),Axios 负责网络请求,通过 getState() 方法实现状态与请求的协同,构建完整业务链路;
  4. 问题解决:针对 Hook 调用错误、Token 过期、重复请求等常见问题,提供了具体的解决方案和优化思路。

6.2 技术展望

Zustand 作为轻量级状态管理库,在中小型项目中具有明显的开发效率优势,未来可结合 React 18 的 Concurrent Mode、Server Components 等新特性,进一步优化状态更新性能;Axios 可结合请求取消、重试机制、缓存策略等,构建更健壮的网络请求层。

在实际项目开发中,需根据项目规模和需求选择合适的技术方案:中小型项目可采用“Zustand + Axios”组合,兼顾开发效率与性能;大型项目可结合 React Query 处理服务端状态,进一步分离本地状态与服务端状态,提升项目可维护性。

通过本文的学习与实践,可熟练掌握 Zustand 与 Axios 的核心用法,避开常见坑点,高效搭建稳定、可扩展的 React 应用。后续需持续关注技术更新,结合实际业务场景不断优化代码,提升开发能力。