为何自定义带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_
相关产品推荐
相关产品推荐

