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

React使用AbortController时组件渲染即报操作中止错误

问题现象

在React项目中自定义useHttpClient Hook处理接口请求,内部接入AbortController API,期望实现用户跳转路由、组件卸载时自动中止未完成的fetch请求,避免内存泄漏和异常报错。实际运行时组件首次渲染完成就直接抛出「操作已被中止」的错误,中止逻辑触发时机不符合预期。

相关实现代码

Hook 实现代码

import { useEffect , useState , useCallback , useRef } from 'react';

export const useHttpClient = () => {
   const [isLoading, setIsLoading] = useState(false);
   const [error, setError] = useState();


   const activeHttpRequests = useRef([]);

   const sendRequest = useCallback(async (URL, method = 'GET', body = null, headers = {}) => {
      setIsLoading(true);


      
      const httpAbortController = new AbortController();
      activeHttpRequests.current.push(httpAbortController);

      try {
         const res = await fetch(URL, {method, body, headers, signal: httpAbortController.signal});
         const resData = await res.json();
         
         activeHttpRequests.current = activeHttpRequests.current.filter(reqCtrl => reqCtrl !== httpAbortController)
         console.log(activeHttpRequests)
         
         if(!res.ok) {
            throw new Error(resData.error);
         }
         setIsLoading(false)   
         return resData;
      } catch (error) {
         setIsLoading(false)   
         setError(error.message)
         throw error;
      }
   }, [])

   const clearError = () => {
      setError(null);
   }


   useEffect(() => {
      return () => {
         activeHttpRequests.current.forEach(abortCtrl => abortCtrl.abort());
      };
   }, []);

   return {isLoading, error, clearError, sendRequest}
}

业务组件使用代码

// Modules
import React, { useEffect, useState } from 'react';

// Hooks 
import { useHttpClient } from '../../../helpers/hooks/http-hook';

// Components
import UserList from '../components/user-list/user-list.component';
import ErrorModal from '../../shared/error-modal/error-modal.component';
import Spinner from '../../shared/loading-spinner/spinner.component';


const Users = () => {
   const { isLoading , error , clearError , sendRequest } = useHttpClient();
   const [loadedUsers, setLoadedUsers] = useState();
   
   useEffect(() => {
      const fetchUsers = async () => {
         try {
            const URL = 'http://localhost:5000/api/users';
            const res = await sendRequest(URL);
            setLoadedUsers(res.users);
         } catch (error) {}
      }
      fetchUsers();
   }, [sendRequest])


   return (
      <React.Fragment>
         <ErrorModal error={error} onClear={clearError} />
         { isLoading && <Spinner overlay/> }
         { !isLoading && loadedUsers && <UserList users={loadedUsers} /> }
      </React.Fragment> 
   );
}
export default Users;
问题根因

触发报错有两个核心原因:

  • React 18 开发环境默认开启严格模式(Strict Mode),会对首次挂载的组件自动执行「挂载→卸载→重新挂载」的模拟流程,提前暴露生命周期逻辑隐患。组件第一次挂载时触发sendRequest,创建的AbortController实例被存入ref,紧接着模拟卸载阶段就会执行useEffect返回的清理函数,调用所有已存储controller的abort()方法,此时请求还在pending状态,直接抛出中止错误。
  • 原有catch逻辑没有区分错误类型,把主动中止请求抛出的错误和真实接口错误同等处理,直接写入error状态,导致页面刚渲染就出现报错。
修复方案

不需要移除严格模式(严格模式可以提前帮助排查代码问题,生产环境不会执行重复挂载的模拟逻辑),只需要在错误捕获逻辑中过滤掉主动中止请求产生的错误即可:AbortController主动中止请求抛出的错误,name属性固定为AbortError,这类错误不需要触发错误提示,也不需要向外抛出。

修复后的核心代码

仅需修改sendRequest方法中的catch块逻辑:

// 省略其余未改动代码
try {
  // 原有请求、响应处理逻辑保持不变
} catch (error) {
  setIsLoading(false);
  // 过滤主动中止请求产生的错误
  if (error.name !== 'AbortError') {
    setError(error.message);
    throw error;
  }
}

修改后,开发环境严格模式下模拟卸载触发的中止错误会被直接忽略,只有真实的接口异常才会触发错误提示;生产环境下用户跳转页面卸载组件时,中止请求产生的错误也不会被误报,完全符合预期效果。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 06:15:46