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

StrictMode下useLayoutEffect中axios重复调用API的问题咨询

React StrictMode下RefreshToken请求重复触发的问题分析与解决

问题根源

你的情况是React StrictMode开发环境下的正常特性暴露了代码中的潜在bug,而非StrictMode本身的问题:

  • StrictMode在开发环境会双重执行组件的副作用(包括useLayoutEffect)、状态更新和部分函数调用,目的是检测那些在重复执行时会异常的代码。
  • 你的代码存在两个核心问题:
    1. useLayoutEffect没有设置依赖数组,导致每次组件渲染都会重复添加axios响应拦截器,即使有清理逻辑,也会在StrictMode的双重执行下出现拦截器多次触发的情况。
    2. 没有处理并发的RefreshToken请求:当第一次401触发刷新请求时,第二次重复的401请求会在浏览器尚未更新新的HttpOnly Cookie时发起,携带旧的RefreshToken,而此时数据库中的旧Token已经被第一次请求更新,导致第二次请求失败。

解决方案

1. 给useLayoutEffect添加空依赖数组

确保拦截器只在组件挂载时注册一次,即使StrictMode双重执行,最终也只会保留一个有效拦截器:

useLayoutEffect(() => {
  const refreshInterceptor = axios.interceptors.response.use(
    (response) => response,
    async (error) => {
      // 原有拦截器逻辑
    }
  );

  return () => {
    axios.interceptors.response.eject(refreshInterceptor);
  };
}, []); // 添加空依赖数组,仅在组件挂载/卸载时执行

2. 添加并发刷新锁与请求队列

防止同一时间多次触发RefreshToken请求,同时缓存等待刷新的请求,在刷新完成后统一重试:
首先在AuthContext中新增状态:

const [isRefreshing, setIsRefreshing] = useState(false);
const [failedRequestsQueue, setFailedRequestsQueue] = useState([]);

然后修改拦截器逻辑:

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

      // 排除RefreshToken自身请求,且标记过_retry的请求不再处理
      if (
        error.response &&
        error.response.status === 401 &&
        originalRequest.url !== "api/v1/refreshToken" &&
        !originalRequest._retry
      ) {
        // 如果正在刷新,将请求加入队列等待
        if (isRefreshing) {
          return new Promise((resolve, reject) => {
            setFailedRequestsQueue(prev => [...prev, { resolve, reject, request: originalRequest }]);
          });
        }

        setIsRefreshing(true);
        try {
          const response = await axios.post("api/v1/refreshToken", {}, { withCredentials: true });

          setToken(response.data.token);
          // 更新原请求的Authorization头
          originalRequest.headers.Authorization = `Bearer ${response.data.token}`;
          originalRequest._retry = true;

          // 重试队列中所有等待的请求
          setFailedRequestsQueue(queue => {
            queue.forEach(({ resolve, request }) => {
              request.headers.Authorization = `Bearer ${response.data.token}`;
              resolve(axios(request));
            });
            return [];
          });

          return axios(originalRequest);
        } catch {
          setToken(null);
          setUser(null);
          // 刷新失败时拒绝队列中所有请求
          setFailedRequestsQueue(queue => {
            queue.forEach(({ reject }) => reject(error));
            return [];
          });
        } finally {
          setIsRefreshing(false);
        }
      }

      return Promise.reject(error);
    }
  );

  return () => {
    axios.interceptors.response.eject(refreshInterceptor);
  };
}, [isRefreshing, setToken, setUser]); // 添加相关依赖

3. 服务端兜底优化(可选)

可以在服务端实现RefreshToken的短窗口有效性:在更新RefreshToken后,保留旧Token在短时间内有效(比如5秒),避免因为前端重复请求导致的验证失败,增强系统的鲁棒性。

总结

StrictMode的双重执行是开发环境的正常检测机制,它帮你发现了代码中拦截器重复注册和并发请求处理缺失的问题。通过上述修复,你的代码可以在保留StrictMode的情况下正常工作,同时也提升了生产环境下的稳定性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 20:26:18