DEBUG.WIKI
丽萨主机;为跨境业务选好 IP;原生 / 住宅 IP · 按套餐选择;查看 IP →
阿里云;从建站到云上业务;云服务器 · 数据库 · 对象存储;查看产品 →
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

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 宣布解决。