React中使用useEffect基于状态控制带延迟的递归fetch请求启停
解决方案
核心问题修复思路
- 使用
useRef存储跨渲染周期的变量:普通组件内变量会在每次重渲染时重置,useRef的current属性可以持久化存储值,且不会触发组件重渲染,适合存储请求实例、定时器ID、最新状态快照 - 采用标准
AbortController实现fetch请求取消:原生fetch不支持直接调用cancel方法,需要绑定控制器的signal参数实现取消 - 用ref同步状态值解决闭包捕获旧值问题:递归函数因为闭包特性会捕获创建时的状态值,将状态同步到ref后访问
current属性即可拿到最新值 - 修复状态更新异步导致的初始值错误:
setState是异步操作,调用后不会立刻更新状态,需要同步更新ref或者直接用接口返回的新值启动递归
修复后完整代码
import { useState, useEffect, useRef } from 'react'; const Component = () => { const [executing, setExecuting] = useState(false); const [data1, setData1] = useState('default'); const [data2, setData2] = useState('default'); const [data3, setData3] = useState('default'); // 存储AbortController实例,用于取消fetch请求 const abortControllerRef = useRef(null); // 存储定时器ID,用于清理延迟任务 const timeoutRef = useRef(null); // 同步存储data2的最新值,解决闭包旧值问题 const data2Ref = useRef(data2); // 同步data2状态到ref useEffect(() => { data2Ref.current = data2; }, [data2]); const recursiveFetch = async () => { // 已停止执行则直接终止递归 if (!executing) return; // 取消上一次未完成的请求 if (abortControllerRef.current) { abortControllerRef.current.abort(); } // 新建请求控制器 const controller = new AbortController(); abortControllerRef.current = controller; try { const res = await fetch(`https://website.com/${data2Ref.current}`, { method: 'POST', signal: controller.signal }); const resData = await res.json(); // 此处可根据需要更新data3等状态 setData3(resData); // 启动下一次延迟请求 timeoutRef.current = setTimeout(() => { recursiveFetch(); }, 1000); } catch (err) { // 忽略主动取消请求的报错 if (err.name !== 'AbortError') { console.error('请求出错:', err); } } }; useEffect(() => { if (executing) { // 拉取必要的初始数据 fetch(`https://example.com/${data1}`) .then((res) => res.json()) .then((data) => { const newData2 = data.exampleProperty; // 同步更新状态和ref setData2(newData2); // 启动递归请求 recursiveFetch(); }); } else { // 执行清理逻辑 if (abortControllerRef.current) { abortControllerRef.current.abort(); } if (timeoutRef.current) { clearTimeout(timeoutRef.current); } } // 组件卸载/依赖变更时自动执行清理 return () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, [executing, data1]); return ( <> <button onClick={() => setExecuting((prev) => !prev)}> {executing ? '停止请求' : '启动请求'} </button> </> ); }; export default Component;
关键改动说明
- 新增的三个ref分别解决了请求实例、定时器的持久化存储,以及data2的最新值获取问题,避免了组件重渲染导致的变量丢失
- 递归函数内增加了执行状态判断,停止后不会继续发起新请求
- 新增了useEffect的清理函数,避免组件卸载后仍有请求或定时器运行导致的内存泄漏
- 将data1加入useEffect依赖,保证data1更新后重启请求时使用最新值
内容的提问来源于stack exchange,提问作者Looky1173
相关产品推荐
相关产品推荐

