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

React加载器在Axios请求pending状态下无法保持显示的问题

嘿,我看你遇到了React Context搭配Axios拦截器控制加载Spinner的棘手问题——明明计数日志显示从0→1→2→1→0是正常的,但Chrome网络面板里请求还处于pending状态时,加载器就提前隐藏了。重构后也没解决,咱们来拆解问题根源,然后给出靠谱的修复方案:

问题根源拆解
  1. hideLoading的定时器逻辑完全无效:你在hideLoading里先设置了3秒定时器要隐藏加载,紧接着就return clearTimeout(loaderKeepAlive.current)——这等于刚创建定时器就立刻清除,根本起不到延迟隐藏的作用,加载器会被直接隐藏。
  2. 请求拦截器的计数更新逻辑有隐患:虽然你的计数日志看起来正常,但requestHandler里直接dispatch({ type: "SET_COUNT", count: 1 })的写法很危险,如果同时发起多个请求,这个逻辑会覆盖之前的计数(除非你的Reducer是累加逻辑,但写法不够直观)。
  3. 拦截器注册的判断方式不可靠:用api.interceptors.request.handlers[0]来判断是否已注册拦截器,容易因为Axios内部数组的变化导致误判,要么重复注册,要么注册失败。
  4. 加载状态控制的定时器耦合过紧:showLoader和loaderKeepAlive两个定时器互相依赖,逻辑复杂,很容易出现时序问题。
修复方案与代码调整

咱们一步步把这些问题理顺,简化逻辑的同时保证状态同步:

1. 先修正LoadingReducer的计数逻辑

把计数改成明确的递增/递减,避免直接赋值的隐患,同时确保count不会变成负数:

// reducer/LoadingReducer.js
const LoadingReducer = (state, action) => {
  switch (action.type) {
    case "INCREMENT_COUNT":
      return {
        ...state,
        count: state.count + 1,
        // 这里可以先不直接设置loading,留给定时器控制防闪屏
      };
    case "DECREMENT_COUNT":
      const newCount = Math.max(0, state.count - 1); // 防止count为负
      return {
        ...state,
        count: newCount,
      };
    case "SHOW_LOADING":
      return { ...state, loading: true };
    case "HIDE_LOADING":
      return { ...state, loading: false };
    default:
      return state;
  }
};

export default LoadingReducer;

2. 简化拦截器逻辑,用ID管理拦截器

用Axios拦截器返回的ID来注册/移除,避免重复注册的问题,同时修正计数的dispatch动作:

import React, { useReducer, useRef, useEffect, useCallback } from "react";
import { api } from "api/api";
import LoadingReducer from "reducer/LoadingReducer";

const LoadingContext = React.createContext();

export const LoadingProvider = ({ children }) => {
  const [loader, dispatch] = useReducer(LoadingReducer, { loading: false, count: 0 });
  // 保存拦截器ID,用于卸载时清理
  const requestInterceptorId = useRef(null);
  const responseInterceptorId = useRef(null);

  // 请求拦截:每次请求计数+1
  const requestHandler = useCallback((request) => {
    dispatch({ type: "INCREMENT_COUNT" });
    return Promise.resolve(request);
  }, [dispatch]);

  // 响应成功:计数-1
  const successHandler = useCallback((response) => {
    dispatch({ type: "DECREMENT_COUNT" });
    return Promise.resolve(response);
  }, [dispatch]);

  // 请求/响应错误:计数-1
  const errorHandler = useCallback((error) => {
    dispatch({ type: "DECREMENT_COUNT" });
    return Promise.reject(error);
  }, [dispatch]);

  // 注册/清理拦截器
  useEffect(() => {
    // 只注册一次拦截器
    if (!requestInterceptorId.current) {
      requestInterceptorId.current = api.interceptors.request.use(requestHandler, errorHandler);
    }
    if (!responseInterceptorId.current) {
      responseInterceptorId.current = api.interceptors.response.use(successHandler, errorHandler);
    }

    // 组件卸载时移除拦截器,防止内存泄漏
    return () => {
      if (requestInterceptorId.current) {
        api.interceptors.request.eject(requestInterceptorId.current);
      }
      if (responseInterceptorId.current) {
        api.interceptors.response.eject(responseInterceptorId.current);
      }
    };
  }, [requestHandler, successHandler, errorHandler]);

  // 根据count控制加载器显示/隐藏,添加短延迟防闪屏
  useEffect(() => {
    let timer;
    if (loader.count > 0) {
      // 300ms延迟:避免快速请求导致加载器一闪而过
      timer = setTimeout(() => {
        dispatch({ type: "SHOW_LOADING" });
      }, 300);
    } else {
      // 300ms延迟:避免请求切换时加载器频繁闪烁
      timer = setTimeout(() => {
        dispatch({ type: "HIDE_LOADING" });
      }, 300);
    }

    // 清理定时器
    return () => clearTimeout(timer);
  }, [loader.count, dispatch]);

  return (
    <LoadingContext.Provider value={{ loader }}>
      {children}
    </LoadingContext.Provider>
  );
};

export default LoadingContext;

3. 关键逻辑说明

  • 拦截器管理:用requestInterceptorId和responseInterceptorId保存拦截器ID,组件卸载时调用eject移除,彻底避免内存泄漏和重复注册问题。
  • 计数逻辑:明确的INCREMENT_COUNT和DECREMENT_COUNT确保每个请求都会正确累加/递减计数,不会出现覆盖的情况。
  • 防闪屏延迟:300ms的延迟可以避免快速请求导致加载器闪屏,同时保证用户等待时能及时看到加载状态。
  • 状态同步:只有当所有请求都完成(count=0)时,才会延迟隐藏加载器,完全匹配网络请求的pending状态。
额外注意事项
  • 确保你的Axios实例api是全局单例,不要每次请求都创建新实例,否则拦截器不会生效。
  • 如果你的请求有重试逻辑,要确保重试时也会触发计数递增,避免计数异常。

这样调整后,加载器就会在至少有一个请求处于pending状态时保持显示,直到所有请求完成(成功或失败),完全解决你遇到的提前隐藏问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 15:32:39