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

如何从自定义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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 14:03:28