如何从自定义Axios组件向LoadingOverlay组件触发事件更新加载状态
问题背景
当前项目组件架构分层如下:
LoginPage => 表单层 useAuth => 控制层 useAuthenticationServices => 服务层 useOpenApi => 带拦截器的自定义Axios封装
触发提交操作(执行handleSubmitClick)时可正常从服务端获取数据,调用链路为:表单提交方法 → useAuth的登录方法 → 服务层登录接口 → 自定义Axios发起请求,该链路功能运行正常。
现存问题:无法在useOpenApi自定义逻辑中调用任何React Hook(包括useContext/useState/useEffect等,调用时会抛出Hook规则错误)。需求是在Axios的请求拦截器逻辑中触发状态更新,控制LoadingOverlay组件的isLoading状态切换为true,排除直接操作DOM修改CSS类的实现方式。
现有代码结构
// (1) 表单层 const LoginPage = (props) => { // (1.1) 提交点击处理 const handleSubmitClick = async (props, context) => { // (1.2) 调用登录方法 const ret = await doLogin(state.username, state.password) // ...其余逻辑 } } // (2) 控制层Hook const useAuth = () => { // (2.1) 登录逻辑 const doLogin = async (username, password) => { // (2.2) 调用服务层登录接口 const ret = await login(username, password) // ...其余逻辑 } } // (3) 服务层Hook const useAuthenticationServices = () => { // (3.1) 登录接口封装 const login = async (email, password) => { // (3.2) 调用自定义Axios发起请求 const ret = await openApi.post('/useraccounts/Login', payload) // ...其余逻辑 } } // (4) Axios封装Hook const useOpenApi = () => { const openApi = axios.create({ baseURL: process.env.SERVICE_URL }); // (4.1) 请求拦截器 —— 需要在这里触发Loading状态更新 openApi.interceptors.request.use(async config => { // ...拦截器逻辑 return config; }) }
组件嵌套层级
<ToastContainer> <MyCustomContext> <LoadingOverlay /> {/* 需要在这里控制isLoading状态 */} <LoginPage /> {/* 内部调用自定义Axios,触发4.1拦截器 */} </LoadingOverlay> </MyCustomContext> </ToastContainer>
核心诉求:寻找自定义Axios实例对接React上下文、向LoadingOverlay派发状态更新事件的最优实现方案,避免直接操作DOM。
最优实现方案
采用事件订阅+全局Loading上下文的方案实现,完全符合Hook规则,分层清晰无侵入:
第一步:抽离独立的Axios实例与事件发射器
不要把Axios实例的创建放在useOpenApi Hook里,把它抽到Hook外面作为单例,搭配一个极简的事件发射器处理状态通知,完全脱离React生命周期,不会触发Hook规则错误:
// 单独存为api/client.js文件,不要写在React Hook内部 import axios from 'axios' // 极简事件发射器,无额外依赖 const createEventHub = () => ({ events: new Map(), on(event, handler) { if (!this.events.has(event)) this.events.set(event, new Set()) this.events.get(event).add(handler) return () => this.events.get(event).delete(handler) }, emit(event, payload) { if (!this.events.has(event)) return this.events.get(event).forEach(handler => handler(payload)) } }) export const apiHub = createEventHub() export const openApi = axios.create({ baseURL: process.env.SERVICE_URL }) // 拦截器里直接发事件,不需要调用任何React Hook let pendingRequestCount = 0 openApi.interceptors.request.use(async config => { pendingRequestCount++ // 发起第一个请求时触发loading开启事件 if (pendingRequestCount === 1) apiHub.emit('loadingChange', true) return config }) // 响应拦截器统一处理loading关闭 const handleResponse = () => { pendingRequestCount = Math.max(0, pendingRequestCount - 1) if (pendingRequestCount === 0) apiHub.emit('loadingChange', false) } openApi.interceptors.response.use(handleResponse, handleResponse)
原有useOpenApi直接引入封装好的实例返回即可,不需要改动上层调用逻辑:
import { openApi } from './api/client' export const useOpenApi = () => { return { openApi } }
第二步:封装全局Loading上下文
在现有上下文层新增Loading上下文,把事件监听放在上下文组件内,对接LoadingOverlay的状态:
// LoadingContext.jsx import { createContext, useContext, useState, useEffect } from 'react' import { apiHub } from './api/client' const LoadingContext = createContext({ isLoading: false, setLoading: () => {} }) export const LoadingProvider = ({ children }) => { const [isLoading, setIsLoading] = useState(false) useEffect(() => { // 组件挂载时监听Axios派发的loading状态变更事件 const unsub = apiHub.on('loadingChange', (status) => { setIsLoading(status) }) // 组件卸载时取消监听,避免内存泄漏 return unsub }, []) return ( <LoadingContext.Provider value={{ isLoading, setLoading: setIsLoading }}> <LoadingOverlay isLoading={isLoading}> {children} </LoadingOverlay> </LoadingContext.Provider> ) } // 导出Hook供业务代码手动控制loading状态 export const useLoading = () => useContext(LoadingContext)
第三步:调整组件层级
用封装好的LoadingProvider替换原有硬编码的LoadingOverlay即可,不需要改动任何业务层代码:
<ToastContainer> <MyCustomContext> <LoadingProvider> <LoginPage /> </LoadingProvider> </MyCustomContext> </ToastContainer>
方案优势
- 完全符合React Hook规则:所有Hook调用都在React组件/自定义Hook顶层执行,Axios层完全脱离React生命周期,不会抛出Hook调用错误
- 自动适配并发请求:通过
pendingRequestCount计数,不会出现单个请求结束就关闭loading、其余请求仍在等待的bug - 分层无侵入:原有业务代码(useAuth、useAuthenticationServices、LoginPage)不需要任何改动,Axios层不需要感知React上下文的存在
- 扩展性强:后续需要手动控制loading状态时,直接调用
useLoading().setLoading(true/false)即可,也可以在事件hub中扩展错误通知、请求日志等全局事件能力 - 轻量无依赖:核心逻辑仅几十行代码,不需要引入Redux、Zustand等额外状态管理库
内容的提问来源于stack exchange,提问作者Silvio
相关产品推荐
相关产品推荐

