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

自定义Fetch Hook加载状态避免无限循环问题求助

问题分析与解决方案

你的核心问题在于:自定义Hook useApi 中,手动触发API调用时设置 setLoading(true) 会引发无限循环,同时需要兼容两种loading场景——自动加载(初始loading=true)和手动触发(初始loading=false,请求前设为true)。

问题根源

  1. 函数重复创建导致无限渲染:每次Hook渲染时,methods 对象及其内部的 get/post 函数都会被重新创建。当你在函数内调用 setLoading(true) 触发状态更新,Hook会重新执行,生成新的函数实例。如果组件中把这些函数作为依赖(比如在 useEffect 或事件回调中),会触发新一轮渲染,形成循环。
  2. AbortController 复用问题:当前Hook初始化时就创建 AbortController,多次调用API会复用同一个控制器,导致之前的请求被意外中止。
  3. 单一loading初始状态:无法区分自动加载和手动触发场景的初始loading需求。

修复方案

以下是修改后的 useApi Hook,解决上述问题并支持两种loading模式:

import { useState, useCallback, useMemo, useEffect } from "react";

export default function useApi({ method, url, autoLoad = true }) {
  // 根据autoLoad设置初始loading状态
  const [loading, setLoading] = useState(autoLoad);

  const fetchBaseUrl = import.meta.env.VITE_APP_URL + '/api/' + url;

  // 用useCallback包裹API方法,避免每次渲染重新创建函数
  const get = useCallback((data = {}) => {
    const abortController = new AbortController();
    setLoading(true);
    
    const params = new URLSearchParams(data);
    const queryString = params.toString();
    const fetchUrl = fetchBaseUrl + (queryString ? "?" + queryString : "");

    return new Promise((resolve, reject) => {
      fetch(fetchUrl, {
        signal: abortController.signal,
        headers: {
          "Content-Type": "application/json",
          "Accept": "application/json",
        },
      })
      .then(response => response.json())
      .then(data => {
        setLoading(false);
        data ? resolve(data) : reject(data);
      })
      .catch(error => {
        setLoading(false);
        if (!abortController.signal.aborted) {
          reject(error);
        }
      });
    });
  }, [fetchBaseUrl]);

  const post = useCallback((data = {}) => {
    const abortController = new AbortController();
    setLoading(true);
    
    const bodyData = { ...data };

    return new Promise((resolve, reject) => {
      fetch(fetchBaseUrl, {
        method: "post",
        headers: {
          "Content-Type": "application/json",
          "Accept": "application/json",
        },
        body: JSON.stringify(bodyData),
        signal: abortController.signal,
      })
      .then(response => response.json())
      .then(data => {
        setLoading(false);
        data ? resolve(data) : reject(data);
      })
      .catch(error => {
        setLoading(false);
        if (!abortController.signal.aborted) {
          reject(error);
        }
      });
    });
  }, [fetchBaseUrl]);

  // 用useMemo缓存methods对象,避免重复创建
  const methods = useMemo(() => ({ get, post }), [get, post]);

  if (!(method in methods)) {
    throw new Error("Incorrect useApi() parameter 'method'");
  }

  const apiCall = methods[method];

  // 自动加载逻辑:如果autoLoad为true,组件挂载时调用一次API
  useEffect(() => {
    if (autoLoad) {
      apiCall();
    }
  }, [apiCall, autoLoad]);

  return [loading, apiCall];
}

关键修改点说明

  1. 新增autoLoad参数:默认true,控制初始loading状态和是否自动触发API调用。手动触发场景只需传入autoLoad: false,初始loading即为false。
  2. useCallback包裹API函数:确保函数实例在依赖不变时不会重复创建,避免因函数引用变化触发的无限渲染。
  3. useMemo缓存methods对象:同样避免每次渲染都生成新对象,稳定函数引用。
  4. AbortController移至方法内部:每次调用API时创建独立的控制器,避免多请求互相干扰。
  5. 自动加载逻辑:通过useEffect在组件挂载时自动调用API,仅依赖稳定的apiCall函数和autoLoad参数。

使用示例

  • 自动加载场景(初始loading=true):
const [loading, fetchData] = useApi({ method: 'get', url: 'users' });
  • 手动触发场景(初始loading=false):
const [loading, submitForm] = useApi({ method: 'post', url: 'login', autoLoad: false });

// 点击按钮触发
const handleSubmit = () => {
  submitForm({ username: 'xxx', password: 'xxx' })
    .then(res => console.log(res))
    .catch(err => console.error(err));
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 01:22:57