React 的定时器一直读到旧 state
setInterval 保存建立时的 state;只依赖上一值时用函数更新,依赖当前参数时安排可清理的 timeout,并保留完整依赖。
计数器的页面已经更新,setInterval 回调里却仍读到初始值。把定时器放进依赖为 [] 的 Effect,并在回调里写 setCount(count + 1),可能让数字只到 1 就停住:回调保存的是建立这一轮 Effect 时的 count。
这是函数组件闭包与状态快照的问题,不是定时器自动取得 React 最新 state。下面的处理适用于 React 18、19 的常规 Hook 用法,不依赖较新版本的 Effect Event 接口。
下一值只由上一值决定时,用函数更新
持续递增只需要上一份 count。把计算交给 state updater,定时器回调就不必读建立时的 count:
import { useEffect, useState } from 'react';
export default function Counter({ delay = 1000 }) {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(previous => previous + 1);
}, delay);
return () => clearInterval(id);
}, [delay]);
return <p>{count}</p>;
}
这里 delay 是有限的正毫秒数,调用方应保证这个前提。它改变时清理旧定时器并创建新定时器;count 改变不需要重新订阅。updater 只计算和返回下一值,保持纯函数,不在其中发请求、写日志作为业务动作或修改外部对象。
函数更新只解决“用上一份 state 计算下一份 state”的部分。如果回调同时读取其他 prop,例如结束上限或当前选中项,那些值仍属于它创建时的闭包,不能因为 count 改成 updater 就把所有依赖都删掉。
回调确实依赖当前值时,允许重新安排
到某个上限就停止的计数器,可以把下一次 tick 表达为一次 timeout。Effect 每次都读取当前 count 和上限,上一轮 timeout 会在依赖变化或卸载时取消:
import { useEffect, useState } from 'react';
export default function BoundedCounter({ limit, delay = 1000 }) {
const [count, setCount] = useState(0);
useEffect(() => {
if (count >= limit) return;
const id = setTimeout(() => setCount(count + 1), delay);
return () => clearTimeout(id);
}, [count, limit, delay]);
return <p>{count}</p>;
}
这是从零递增至上限的局部组件,limit 为非负整数、delay 为正毫秒数。limit 降低不会主动把当前值截回去;若业务需要重置,应另外定义重置行为。此写法按提交后重新安排下一次等待,不是精确钟表,不适合用 tick 次数代表真实经过的秒数。
保留长期监听器而又必须读取最新值时,可以考虑显式维护 ref,但 ref 更新不会触发渲染,也不能拿它替代本该进入依赖的订阅参数。先区分“订阅应随参数改变”和“回调只需要最新计算值”,再选方案。
检查每次卸载、delay 改变和重新挂载后是否只剩应有的定时器;旧回调不能继续更新页面。若是多个异步请求结果乱序覆盖,还需要独立处理请求生命周期,不能仅靠函数形式的 setState 宣布解决。


