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

为何自定义带AbortController的HTTP Hook设置resData时触发无限循环?

为什么开启setResData后自定义HTTP Hook会触发无限循环?

问题根源

无限循环的核心原因是**useEffect的依赖项opts是引用类型(对象)**,结合状态更新的连锁反应导致:

  • React对useEffect的依赖项采用浅比较:只要对象的引用发生变化,就会判定依赖更新,触发useEffect重新执行。
  • 当你开启setResData(data)时,状态更新会触发组件重新渲染;如果父组件传递给useHttp的opts是内联创建的对象(比如useHttp('/api', { method: 'POST' })),每次组件渲染都会生成一个全新的opts对象(引用不同)。
  • 新的opts引用会触发useEffect再次执行,重新发起请求,请求完成后又调用setResData更新状态,再次触发组件渲染,形成无限循环。

为什么关闭setResData时没出现循环?

关闭setResData后,Hook内部没有状态更新,组件不会因为Hook的状态变化而重新渲染。除非父组件本身有其他状态变化,否则useEffect只会执行一次——但这只是暂时的,只要父组件有其他渲染触发,同样会因为opts引用变化导致重复请求。

解决方案

方案1:在父组件中缓存opts,保证引用稳定

使用useMemo缓存opts对象,只有当依赖的属性变化时才重新创建对象:

import { useMemo } from 'react';

// 父组件中
const postData = { id: 1, name: 'test' };
const opts = useMemo(() => ({
  method: 'POST',
  body: JSON.stringify(postData),
  headers: { 'Content-Type': 'application/json' }
}), [postData]); // 只有postData变化时,opts才会重新创建

const { resData, isLoading } = useHttp('/api/submit', opts);

方案2:在Hook内部解构opts,依赖具体属性

不要直接依赖整个opts对象,而是解构出需要的属性,把这些具体属性作为useEffect的依赖项:

const useHttp = (url = '', opts = {}) => {
  const [resData, setResData] = useState(null);
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [status, setStatus] = useState(null);

  useEffect(() => {
    if (url === '') return;

    const controller = new AbortController();
    const { signal } = controller;
    // 解构opts,提取需要的属性并设置默认值
    const { 
      method = 'GET', 
      body, 
      headers = { 'Content-Type': 'application/json' } 
    } = opts;

    setIsLoading(true);

    const fetchData = async () => {
      try {
        const response = await fetch(url, {
          method,
          body,
          headers,
          credentials: 'include',
          signal,
        });

        const data = await response.json();
        setResData(data); // 现在开启也不会循环了
        setError(null);
        setIsLoading(false);
        setStatus(response.status);
      } catch (err) {
        setResData(null);
        setError(err);
        setIsLoading(false);
        setStatus(500);
      }
    };

    fetchData();

    return () => controller.abort();
  }, [url, opts.method, opts.body, opts.headers]); // 依赖具体属性而非整个对象

  return { resData, error, isLoading, status };
};

export default useHttp;

方案3:使用JSON.stringify处理依赖(不推荐,仅作备选)

如果不想解构属性,也可以把opts转为字符串作为依赖,但这种方式性能较差,且对复杂对象可能存在序列化问题:

useEffect(() => {
  // ... 逻辑不变
}, [url, JSON.stringify(opts)]);

总结

同类示例中之所以没出现循环,要么是父组件传递的opts本身引用稳定(比如定义在组件外部,或用useMemo缓存),要么是Hook内部处理了依赖项的稳定性。只要保证useEffect的依赖项引用不随无关渲染变化,就能彻底解决这个无限循环问题。

内容的提问来源于stack exchange,提问作者Carl.t_

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 22:07:52