React useEffect返回函数中clearTimeout失效问题排查
问题根源分析
你的问题出在两个关键地方:
1. 错误的状态更新方式导致timers数组未正确更新
你在setTimers里直接修改了React状态数组的引用:
setTimers((prev) => { prev.push(timer) return prev });
React判断状态是否变化是基于浅比较,你直接对prev数组执行push(修改原数组),然后返回同一个引用,React会认为状态没有改变,因此不会更新timers的状态值。最终清理函数里的timers还是初始的空数组,自然无法清除任何定时器。
2. 闭包导致清理函数捕获的是旧的状态值
即使你正确更新了状态,因为你的useEffect依赖项是空数组,这个useEffect只会在组件首次渲染时执行,清理函数会捕获首次渲染时的timers状态值(也就是空数组)。后续对timers的状态更新不会影响清理函数里的变量引用,所以清理时依然无法访问到所有定时器ID。
修复方案
最可靠的方式是使用useRef存储定时器数组,因为useRef的current属性是可变的,不会触发组件重新渲染,而且清理函数能直接访问到最新的定时器列表:
import { useEffect, useRef } from 'react'; // 用useRef替代useState存储定时器 const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]); useEffect(() => { for (let index = 0; index < 12; index++) { const delay = (index + 1) * 10000; const timer = setTimeout(() => { checkProgress(id, setFieldValue, contractType, fileOCRStatus); }, delay); timersRef.current.push(timer); } // 清理函数 return () => { timersRef.current.forEach(timer => clearTimeout(timer)); // 清空数组,避免内存泄漏 timersRef.current = []; }; }, []); // 根据实际业务场景,可添加id等相关依赖项
如果一定要用useState,必须确保每次更新都返回新数组(比如setTimers(prev => [...prev, timer])),但这种方式会因为闭包问题需要额外处理依赖,反而容易出错,因此优先选择useRef方案。
额外注意:如果checkProgress已经发起了API请求,clearTimeout只能取消尚未执行的定时器,无法终止已经发出的请求。如果需要取消正在进行的API调用,要使用AbortController来实现。
内容的提问来源于stack exchange,提问作者margherita pizza
相关产品推荐
相关产品推荐

