You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中useState/useRef更新异常:计时器无法正常切换

问题根源:闭包捕获的过时状态值

你的代码问题核心是React闭包特性导致的状态值捕获过时:

当调用startStop启动计时器时,内部的step函数会捕获当前组件渲染时的switchTimers函数。而switchTimers里的timerDisplay是当时的状态快照——比如第一次启动Session计时器时,timerDisplay是"Session",这个值会被永久保存在闭包中,不会随后续状态更新而改变。

当Session计时器结束,调用的是这个旧的switchTimers,它会将timerDisplay切换为"Break"并启动Break计时器,但此时启动Break计时器的startStop依然是旧版本,内部的step函数捕获的还是依赖"Session"状态的switchTimers。所以当Break计时器结束时,调用的依然是旧的switchTimers,判断逻辑仍认为当前是"Session",从而再次启动Break计时器,无法切换回Session。

解决方法:用useRef跟踪最新状态值

React的useRef创建的引用是可变的,且能在闭包中获取到最新值,不受组件渲染周期影响。我们可以用它同步timerDisplay的状态,确保switchTimers总能拿到最新的显示状态。

修改后的代码示例

import { useState, useRef, useEffect } from 'react';

// ... 其他组件代码

const [timers, setTimers] = useState({
  sessionTime: 25,
  breakTime: 5,
});
const [timerDisplay, setTimerDisplay] = useState("Session");
const [timerActive, setTimerActive] = useState(false)
const [displayCount, setDisplayCount] = useState(1500);
const round = useRef();
// 新增ref跟踪最新的timerDisplay状态
const timerDisplayRef = useRef(timerDisplay);

// 同步状态到ref,确保ref始终持有最新值
useEffect(() => {
  timerDisplayRef.current = timerDisplay;
}, [timerDisplay]);

function startStop(action, secondsLeft) {
  const interval = 1000;
  let expected = Date.now() + interval;
  if (action === "start") {
    setTimerActive(true)
    round.current = setTimeout(step, interval);
    function step() {
      if (secondsLeft > 0) {
        const drift = Date.now() - expected;
        setDisplayCount((prevValue) => prevValue - 1);
        secondsLeft --
        expected += interval;
        round.current = setTimeout(step, Math.max(0, interval - drift));
      } else {
        clearTimeout(round.current)
        switchTimers()
      }
    }
  } else if (action === "stop") {
    clearTimeout(round.current);
    setTimerActive(false);
  }
}

function switchTimers() {
  // beep.current.play()
  // 使用ref中的最新值进行判断
  const currentDisplay = timerDisplayRef.current;
  if (currentDisplay === "Session") {
    setTimerDisplay("Break");
    setDisplayCount(timers.breakTime * 60);
    startStop("start", timers.breakTime * 60)
  } else if (currentDisplay === "Break") {
    setTimerDisplay("Session");
    setDisplayCount(timers.sessionTime * 60);
    startStop("start", timers.sessionTime * 60)
  }
}

// ... 其他组件代码

额外优化建议

  • 每次启动新计时器前,建议先执行clearTimeout(round.current),避免多个定时器同时运行(当前代码中step结束时已清除,但如果有其他场景触发启动,可在startStop的start分支开头添加该操作)。
  • 可以将计时器结束的判断改为依赖displayCount状态,而非传入的secondsLeft参数,避免局部变量与状态不一致的问题。

内容的提问来源于stack exchange,提问作者oojean

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 18:33:35