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 了! 🚀

相关推荐
_r0bin_18 分钟前
前端面试准备-7
开发语言·前端·javascript·fetch·跨域·class
IT瘾君19 分钟前
JavaWeb:前端工程化-Vue
前端·javascript·vue.js
potender22 分钟前
前端框架Vue
前端·vue.js·前端框架
站在风口的猪11081 小时前
《前端面试题:CSS预处理器(Sass、Less等)》
前端·css·html·less·css3·sass·html5
程序员的世界你不懂1 小时前
(9)-Fiddler抓包-Fiddler如何设置捕获Https会话
前端·https·fiddler
MoFe11 小时前
【.net core】天地图坐标转换为高德地图坐标(WGS84 坐标转 GCJ02 坐标)
java·前端·.netcore
去旅行、在路上2 小时前
chrome使用手机调试触屏web
前端·chrome
Aphasia3112 小时前
模式验证库——zod
前端·react.js
lexiangqicheng3 小时前
es6+和css3新增的特性有哪些
前端·es6·css3
拉不动的猪4 小时前
都25年啦,还有谁分不清双向绑定原理,响应式原理、v-model实现原理
前端·javascript·vue.js