自定义Fetch Hook加载状态避免无限循环问题求助
问题分析与解决方案
你的核心问题在于:自定义Hook useApi 中,手动触发API调用时设置 setLoading(true) 会引发无限循环,同时需要兼容两种loading场景——自动加载(初始loading=true)和手动触发(初始loading=false,请求前设为true)。
问题根源
- 函数重复创建导致无限渲染:每次Hook渲染时,
methods对象及其内部的get/post函数都会被重新创建。当你在函数内调用setLoading(true)触发状态更新,Hook会重新执行,生成新的函数实例。如果组件中把这些函数作为依赖(比如在useEffect或事件回调中),会触发新一轮渲染,形成循环。 - AbortController 复用问题:当前Hook初始化时就创建
AbortController,多次调用API会复用同一个控制器,导致之前的请求被意外中止。 - 单一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]; }
关键修改点说明
- 新增
autoLoad参数:默认true,控制初始loading状态和是否自动触发API调用。手动触发场景只需传入autoLoad: false,初始loading即为false。 useCallback包裹API函数:确保函数实例在依赖不变时不会重复创建,避免因函数引用变化触发的无限渲染。useMemo缓存methods对象:同样避免每次渲染都生成新对象,稳定函数引用。- AbortController移至方法内部:每次调用API时创建独立的控制器,避免多请求互相干扰。
- 自动加载逻辑:通过
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
相关产品推荐
相关产品推荐

