You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Axios刷新令牌问题:React useEffect请求令牌过期后状态异常解决方案咨询

React useEffect中Axios刷新令牌重试请求后loading状态异常的问题

我在使用React.useEffect()获取用户列表时遇到了一个问题:当请求因令牌过期返回401时,我的Axios响应拦截器会刷新令牌并重试原请求,但此时useEffect已经进入错误分支,将loading状态设为false,导致用户列表显示旧状态。

我的useEffect代码:

React.useEffect(() => { 
  dispatch(UsersActions.creators.fetchingUsersAction()); 
  UsersApi.methods.getUsers().then( 
    (res) => { 
      dispatch(UsersActions.creators.fetchUsersSuccessAction(res.data)); 
    }, 
    (e) => { 
      dispatch(UsersActions.creators.fetchUsersErrorAction()); 
    } 
  ); 
}, [dispatch]);

其中fetchingUsersAction用于将loading设为true,fetchUsersErrorAction用于将其设为false。除了令牌过期的情况外,其他场景都正常工作。

我的Axios响应拦截器代码:

ApiClient.interceptors.response.use( 
  function (response) { 
    return response; 
  }, 
  function (error) { 
    const originalRequest = error.config; 
    if (error.response.status === 401 && !originalRequest._retry) { 
      originalRequest._retry = true; 
      const refresh = JSON.stringify({ refreshToken: localStorage.getItem("refresh") }); 
      AuthenticationApi.methods.refresh(refresh).then((res) => { 
        if (res.data.accessToken) { 
          localStorage.setItem("token", res.data.accessToken); 
        } 
        ApiClient.defaults.headers.common["Authorization"] = "Bearer " + res.data.accessToken; 
        originalRequest.headers["Authorization"] = "Bearer " + res.data.accessToken; 
        return ApiClient(originalRequest); 
      }); 
    } 
    return Promise.reject(error); 
  } 
);

请问该如何解决这个问题?


问题根源

你的拦截器在处理401时,没有将重试请求的Promise返回给原请求链,而是直接执行了return Promise.reject(error),导致useEffect里的请求立即进入错误分支,提前把loading设为false,而后续的重试结果完全没被原请求的then/catch捕获。

解决方案

我们需要修改拦截器,让它在处理401时返回重试请求的Promise,这样原请求的then/catch会等待重试完成后再执行,同时优化多请求并发401的场景,避免重复刷新令牌。

1. 修复拦截器的Promise链

修改拦截器代码,确保在处理401时返回整个刷新+重试的Promise链:

// 新增全局变量,处理并发401的情况
let isRefreshing = false;
let failedRequestsQueue = [];

ApiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    // 处理401且不是重试请求的情况
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      // 如果正在刷新令牌,把当前请求加入队列等待
      if (isRefreshing) {
        return new Promise((resolve, reject) => {
          failedRequestsQueue.push({ resolve, reject });
        }).then((newToken) => {
          originalRequest.headers["Authorization"] = `Bearer ${newToken}`;
          return ApiClient(originalRequest);
        }).catch((refreshError) => {
          return Promise.reject(refreshError);
        });
      }

      isRefreshing = true;
      try {
        // 刷新令牌
        const refreshPayload = JSON.stringify({ 
          refreshToken: localStorage.getItem("refresh") 
        });
        const refreshRes = await AuthenticationApi.methods.refresh(refreshPayload);
        const newAccessToken = refreshRes.data.accessToken;

        if (newAccessToken) {
          // 更新本地存储和默认请求头
          localStorage.setItem("token", newAccessToken);
          ApiClient.defaults.headers.common["Authorization"] = `Bearer ${newAccessToken}`;

          // 重试队列中所有等待的请求
          failedRequestsQueue.forEach((request) => {
            request.resolve(newAccessToken);
          });
          failedRequestsQueue = [];
        }

        // 重试原请求
        originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`;
        return ApiClient(originalRequest);
      } catch (refreshError) {
        // 刷新令牌失败,拒绝所有排队的请求
        failedRequestsQueue.forEach((request) => {
          request.reject(refreshError);
        });
        failedRequestsQueue = [];

        // 这里可以添加跳转到登录页、清除本地令牌等逻辑
        localStorage.removeItem("token");
        localStorage.removeItem("refresh");
        window.location.href = "/login";

        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    // 非401错误,直接拒绝
    return Promise.reject(error);
  }
);

2. 确认状态更新逻辑

确保你的fetchUsersSuccessAction也会将loading设为false(通常成功后也需要结束加载状态),这样整个流程的状态变化会是:

  • 触发fetchingUsersAction → loading: true
  • 请求发送,遇到401,拦截器开始刷新令牌
  • 刷新成功,重试原请求
  • 重试成功 → 触发fetchUsersSuccessAction → loading: false,更新用户列表
  • 刷新失败 → 触发fetchUsersErrorAction → loading: false,处理错误

3. 优化useEffect的错误处理(可选)

你可以把catch改成链式调用,让代码更清晰:

React.useEffect(() => { 
  dispatch(UsersActions.creators.fetchingUsersAction()); 
  UsersApi.methods.getUsers()
    .then((res) => {
      dispatch(UsersActions.creators.fetchUsersSuccessAction(res.data));
    })
    .catch((e) => {
      dispatch(UsersActions.creators.fetchUsersErrorAction());
      // 这里可以添加错误提示,比如Toast
    });
}, [dispatch]);

效果

修改后,当令牌过期时,原请求会等待拦截器完成令牌刷新和重试流程,只有当最终请求失败(包括刷新令牌也失败)时,才会进入错误分支设置loading: false,避免了提前结束加载状态导致的旧数据显示问题。


内容的提问来源于stack exchange,提问作者Guilherme

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 09:42:28