useEffect包裹setInterval/timeout时为何无法获取最新状态值
React中useEffect内定时器无法获取最新状态问题
问题表现
useEffect逻辑内部包裹setInterval或setTimeout定时器时,定时器回调中无法获取到state的最新更新值,所有读取到的状态始终停留在初始值- 点击启停按钮无法正常控制计时逻辑
- 控制台打印的状态值不会随更新变化
问题复现代码
import { useState, useEffect } from "react"; let timerRef; const StopWatch = () => { const [time, setTime] = useState(0); const [isPause, setPause] = useState(false); useEffect(() => { timerRef = setInterval(() => { console.log({ time }); if (!isPause) { setTime(time + 1); } }, 5000); return () => clearInterval(timerRef); }, []); const startWatch = () => { setPause(false); }; const stopWatch = () => { setPause(true); }; return ( <> time: {time} <button onClick={startWatch}>Start</button> <button onClick={stopWatch}>stop</button> </> ); }; export { StopWatch };

问题原因
本质是JavaScript闭包陷阱:示例中给useEffect传入了空依赖数组[],意味着effect逻辑只会在组件首次挂载时执行一次,此时创建的定时器回调,捕获的是组件首次渲染时的状态上下文,time和isPause两个值永远是初始状态。后续state更新触发组件重渲染时,不会重新执行effect创建新的定时器回调,旧回调持有的闭包引用始终指向首次渲染的状态值,自然读不到更新。
解决方案
方案1:useRef同步可变状态 + 函数式状态更新
通过useRef存储需要在定时器中读取的可变状态,每次状态更新时同步到ref的current属性上;更新state时使用函数式写法,不依赖闭包中缓存的state值,永远基于最新状态计算。这种方案不需要重建定时器,计时精度更高。
import { useState, useEffect, useRef } from "react"; const StopWatch = () => { const [time, setTime] = useState(0); const [isPause, setPause] = useState(false); // 用ref存储定时器实例、最新的暂停状态 const timerRef = useRef(null); const isPauseRef = useRef(isPause); // 暂停状态更新时同步到ref useEffect(() => { isPauseRef.current = isPause; }, [isPause]); useEffect(() => { timerRef.current = setInterval(() => { if (!isPauseRef.current) { // 函数式更新不依赖闭包缓存的time,永远取最新值计算 setTime(prevTime => prevTime + 1); } }, 1000); return () => clearInterval(timerRef.current); }, []); const startWatch = () => setPause(false); const stopWatch = () => setPause(true); return ( <> time: {time} <button onClick={startWatch}>Start</button> <button onClick={stopWatch}>stop</button> </> ); }; export { StopWatch };
方案2:正确声明useEffect依赖项
把定时器回调中用到的所有state都加入useEffect的依赖数组,每次依赖变化时,会先执行上一轮effect的清理函数清除旧定时器,再重新执行effect创建新的定时器,新回调就能捕获到最新的state值。
import { useState, useEffect, useRef } from "react"; const StopWatch = () => { const [time, setTime] = useState(0); const [isPause, setPause] = useState(false); const timerRef = useRef(null); useEffect(() => { timerRef.current = setInterval(() => { console.log({ time }); if (!isPause) { setTime(time + 1); } }, 1000); return () => clearInterval(timerRef.current); // 将回调中用到的state加入依赖数组 }, [time, isPause]); const startWatch = () => setPause(false); const stopWatch = () => setPause(true); return ( <> time: {time} <button onClick={startWatch}>Start</button> <button onClick={stopWatch}>stop</button> </> ); }; export { StopWatch };
注意:该方案每次time更新都会销毁重建定时器,实际计时间隔容易出现偏差,对时间精度要求高的场景优先选择方案1。
内容的提问来源于stack exchange,提问作者Akhilesh Kumar
相关产品推荐
相关产品推荐

