Axios刷新令牌问题:React useEffect请求令牌过期后状态异常解决方案咨询
我在使用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

