Zustand + Axios 存储 Token 最佳实践

前言:在 React 应用中,通常需要管理用户登录状态和 Token,并确保每次请求都携带 Token。结合 Zustand(状态管理) 和 Axios(HTTP 请求) ,可以实现高效、安全的 Token 存储和自动注入。

1. 方案设计

核心目标

  • ✅ Token 存储 :登录后存储 Token,并持久化(如 localStorage)
  • ✅ 自动注入 Token:每次请求自动携带 Token
  • ✅ Token 过期处理:拦截 401 错误,跳转登录页
  • ✅ 全局状态管理:用户登录状态共享

技术栈

  • Zustand:管理 Token 和用户状态
  • Axios:封装 HTTP 请求,拦截器自动注入 Token
  • 持久化中间件 :zustand/middleware 持久化 Token

2. 代码实现

2.1 安装依赖

csharp 复制代码
npm install zustand axios
# 或
yarn add zustand axios
# 或
pnpm add zustand axios

2.2 创建 Zustand Store(管理 Token 和用户状态)

typescript 复制代码
// store/authStore.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";

type AuthState = {
  token: string | null;
  isAuthenticated: boolean;
  login: (token: string) => void;
  logout: () => void;
};

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      token: null,
      isAuthenticated: false,
      login: (token: string) => set({ token, isAuthenticated: true }),
      logout: () => set({ token: null, isAuthenticated: false }),
    }),
    {
      name: "auth-storage", // localStorage 的 key
    }
  )
);

2.3 封装 Axios(自动携带 Token + 401 拦截)

javascript 复制代码
// utils/api.ts
import axios from "axios";
import { useAuthStore } from "../store/authStore";

const api = axios.create({
  baseURL: "https://your-api.com",
});

// 请求拦截器:自动注入 Token
api.interceptors.request.use((config) => {
  const { token } = useAuthStore.getState();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 响应拦截器:处理 401 错误(Token 过期)
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      useAuthStore.getState().logout(); // 清除 Token
      window.location.href = "/login"; // 跳转登录页
    }
    return Promise.reject(error);
  }
);

export default api;

2.4 使用示例

(1) 登录(存储 Token)

javascript 复制代码
// Login.tsx
import { useState } from "react";
import { useAuthStore } from "../store/authStore";
import api from "../utils/api";

function Login() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const login = useAuthStore((state) => state.login);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      const res = await api.post("/auth/login", { email, password });
      login(res.data.token); // 存储 Token
    } catch (error) {
      alert("Login failed");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Login</button>
    </form>
  );
}

(2) 受保护路由(检查登录状态)

javascript 复制代码
// Dashboard.tsx
import { useEffect } from "react";
import { useAuthStore } from "../store/authStore";
import api from "../utils/api";

function Dashboard() {
  const isAuthenticated = useAuthStore((state) => state.isAuthenticated);

  useEffect(() => {
    if (!isAuthenticated) {
      window.location.href = "/login";
    }
  }, [isAuthenticated]);

  const fetchData = async () => {
    try {
      const res = await api.get("/user/profile"); // 自动携带 Token
      console.log(res.data);
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div>
      <h1>Dashboard</h1>
      <button onClick={fetchData}>Load Data</button>
    </div>
  );
}

(3) 登出(清除 Token)

javascript 复制代码
// LogoutButton.tsx
import { useAuthStore } from "../store/authStore";

function LogoutButton() {
  const logout = useAuthStore((state) => state.logout);

  return <button onClick={logout}>Logout</button>;
}

3. 优化点

(1) Token 自动刷新(JWT)

如果 Token 过期时间较短,可以在拦截器中自动刷新:

ini 复制代码
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      try {
        const res = await api.post("/auth/refresh-token");
        useAuthStore.getState().login(res.data.token); // 更新 Token
        return api(originalRequest); // 重试请求
      } catch (err) {
        useAuthStore.getState().logout();
        window.location.href = "/login";
      }
    }
    return Promise.reject(error);
  }
);

(2) 类型安全(TypeScript)

typescript 复制代码
// 增强 Axios 请求类型
declare module "axios" {
  interface AxiosRequestConfig {
    _retry?: boolean; // 用于 Token 刷新
  }
}

(3) 测试 Mock

php 复制代码
// 测试时 Mock Axios
jest.mock("axios");
const mockedAxios = axios as jest.Mocked<typeof axios>;
mockedAxios.post.mockResolvedValue({ data: { token: "fake-token" } });

4. 对比 Redux + Redux-Thunk

方案 Zustand + Axios Redux + Redux-Thunk
代码量 更简洁(~50行) 需要 actions/reducers(~100行)
性能 更高(按需更新) 依赖 useSelector 优化
异步管理 直接使用 async/await 需要 createAsyncThunk
持久化 内置支持(persist) 需要 redux-persist
适用场景 中小型应用 大型复杂应用

5. 总结

  • 推荐使用 Zustand + Axios:代码更简洁,适合大多数 React 应用。

  • 关键点:

    1. Token 存储 :使用 zustand/middleware 持久化。
    2. Axios 拦截器:自动注入 Token + 处理 401 错误。
    3. 安全优化:可增加 Token 自动刷新、请求重试。
  • 完整示例 :GitHub Repo

这样,你的 React 应用就能安全、高效地管理 Token 了! 🚀

相关推荐
鬼手点金2 小时前
nanoGPT/nanochat 能做出 DeepSeek/Qwen 这样的大模型吗?
服务器·前端·javascript·bug·openclaw
蛋蛋的就会顺顺的2 小时前
hot100 p0——栈,贪心算法,动态规划
java·前端·javascript·算法·leetcode·力扣
程序员Sunday3 小时前
TypeScript 的 any、unknown、never 有什么区别?
前端·面试·typescript
朱涛的自习室3 小时前
70 美金的 Beyond Compare,我让 Gemini 4 和 DeepSeek 造个开源免费版(Day 2)
android·前端·人工智能
码艺-Alimjan3 小时前
Tauri 源码备份 · 打包 & 解压 总结,源码收集之路
前端·javascript·vue.js
小马不起床3 小时前
零基础玩转 FastAPI_11:前端后台管理页面
前端·状态模式·fastapi
子非鱼a5 小时前
【文件上传】一个普普通通的edu16
前端
美狐美颜SDK开放平台5 小时前
直播APP如何实现美白滤镜功能?视频美颜SDK技术开发方案详解
前端·数据库·人工智能·音视频·美颜sdk·直播美颜sdk·视频美颜sdk
pride.li6 小时前
第03章 Python 教程-基础语法:注释与变量
服务器·前端·后端·python·算法·计算机视觉
屏什么录6 小时前
电脑如何录屏幕视频和声音?从工具到代码,7 种方案帮你彻底搞定
前端·音视频开发