自定义useFetch钩子出现无限循环,寻求原因分析与修复方案
解决自定义useFetch钩子的无限循环问题
我来帮你拆解这个useFetch钩子无限循环的问题,其实核心原因和React的依赖比较逻辑有关,咱们一步步来看:
问题根源
你遇到的无限循环,本质是React对useEffect依赖项的浅比较机制导致的:
- 当你在组件内直接传入
init={}(取消注释后),每次组件渲染时都会创建一个全新的空对象(引用地址和上一次完全不同)。 - 而自定义useFetch钩子的
useEffect依赖数组里包含了init这个参数,React发现init的引用变了,就会重新执行useEffect里的fetch逻辑。 - 请求完成后组件会因为数据更新重渲染,又生成新的
init对象,触发新一轮的useEffect,最终形成无限循环。
两种可行的修复方案
方案1:让init保持稳定的引用(推荐,改动最小)
既然问题出在init的引用频繁变化,那我们可以用useMemo把它包裹起来,确保只有当依赖项变化时才重新创建这个对象:
import { useMemo } from 'react'; function App() { // 用useMemo固定init的引用,空依赖数组确保只创建一次 const init = useMemo(() => ({ // 如果后续需要添加请求配置(比如headers、method),直接写在这里 }), []); const { data } = useFetch( "https://jsonplaceholder.typicode.com/todos/1", (res) => ({ ...res, title: res.title + " transformed" }), init ); return <div className="App">{data?.title}</div>; }
如果你的init需要依赖组件内的状态(比如token),只需要把依赖项加到useMemo的第二个数组里即可,比如useMemo(() => ({ headers: { Authorization: token } }), [token])。
方案2:修改useFetch钩子,支持深比较
如果你希望钩子本身能处理这种情况,不用调用方每次都处理init的引用,可以在钩子内部用useRef保存上一次的init,再通过深比较判断是否真的需要重新发起请求:
import { useState, useEffect, useRef } from 'react'; // 可以用lodash的isEqual,或者自己实现一个简单的深比较函数 import { isEqual } from 'lodash'; function useFetch(url, transform = (res) => res, init = {}) { const [data, setData] = useState(null); const prevInitRef = useRef(); // 保存上一次的init引用 useEffect(() => { // 只有当init的内容真正变化时,才执行请求 if (!isEqual(prevInitRef.current, init)) { const fetchData = async () => { try { const response = await fetch(url, init); const result = await response.json(); setData(transform(result)); } catch (error) { console.error('Fetch error:', error); } }; fetchData(); prevInitRef.current = init; // 更新保存的init } }, [url, transform, init]); // 依赖数组保留原参数,但内部做深比较判断 return { data }; }
这个方案的好处是调用方不需要关心init的引用问题,钩子会自己判断是否需要重新请求。
总结
本质上这个问题是React依赖比较的特性导致的,只要解决init引用频繁变化的问题,就能终止无限循环。优先推荐方案1,因为改动小且符合React的最佳实践;如果钩子需要更健壮的兼容性,可以用方案2。
内容的提问来源于stack exchange,提问作者FbnKlmn
相关产品推荐
相关产品推荐

