React加载器在Axios请求pending状态下无法保持显示的问题
嘿,我看你遇到了React Context搭配Axios拦截器控制加载Spinner的棘手问题——明明计数日志显示从0→1→2→1→0是正常的,但Chrome网络面板里请求还处于pending状态时,加载器就提前隐藏了。重构后也没解决,咱们来拆解问题根源,然后给出靠谱的修复方案:
问题根源拆解
hideLoading的定时器逻辑完全无效:你在hideLoading里先设置了3秒定时器要隐藏加载,紧接着就return clearTimeout(loaderKeepAlive.current)——这等于刚创建定时器就立刻清除,根本起不到延迟隐藏的作用,加载器会被直接隐藏。- 请求拦截器的计数更新逻辑有隐患:虽然你的计数日志看起来正常,但
requestHandler里直接dispatch({ type: "SET_COUNT", count: 1 })的写法很危险,如果同时发起多个请求,这个逻辑会覆盖之前的计数(除非你的Reducer是累加逻辑,但写法不够直观)。 - 拦截器注册的判断方式不可靠:用
api.interceptors.request.handlers[0]来判断是否已注册拦截器,容易因为Axios内部数组的变化导致误判,要么重复注册,要么注册失败。 - 加载状态控制的定时器耦合过紧:
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
相关产品推荐
相关产品推荐

