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

React组件函数状态实例不一致原因及定时器实现方案

React定时器状态不一致问题及正确实现方式

问题描述

我尝试用setTimeout和useState在React中实现定时器,runTimer函数负责计时,预期当timerRunning为false时停止递归调用,但点击Start再点Stop后计数器仍在增长。

在组件主体和runTimer里打印timerRunning和seconds,发现二者值不一样:App组件里的状态已经更新,但runTimer里还是旧值,导致定时器停不下来。

代码示例

import "./styles.css";
import { useState } from "react";

export default function App() {
  const [timerRunning, setTimerRunning] = useState(false);
  const [seconds, setSeconds] = useState(0);

  console.log("App body --------------");
  console.log({ timerRunning, seconds });

  function runTimer() {
    console.log("*********** runTimer **********");
    console.log({ timerRunning, seconds });
    if (timerRunning) {
      setTimeout(() => {
        setSeconds((prev) => prev + 1);
        runTimer();
      }, 1000);
    }
  }

  const startTimer = () => {
    console.log("startTimer");
    setTimerRunning(true);
    runTimer();
  };
  const stopTimer = () => {
    // Complete this function
    console.log("stopTimer");
    setTimerRunning(false);
  };
  const resetTimer = () => {
    // Complete this function
    console.log("resetTimer");
    setTimerRunning(false);
    setSeconds(0);
  };
  return (
    <div className="container">
      <h1>Timer</h1>
      <span> 0 mins </span>
      <span> {seconds} secs</span>
      <div>
        <button onClick={startTimer}>Start</button>
        <button onClick={stopTimer}>Stop</button>
        <button onClick={resetTimer}>Reset</button>
      </div>
    </div>
  );
}

控制台日志片段

index.js:27 App body --------------
index.js:27 {timerRunning: true, seconds: 9}
index.js:27 *********** runTimer **********
index.js:27 {timerRunning: true, seconds: 0}
index.js:27 App body --------------
index.js:27 {timerRunning: true, seconds: 10}
index.js:27 App body --------------
index.js:27 {timerRunning: true, seconds: 10}
index.js:27 stopTimer
index.js:27 App body --------------
index.js:27 {timerRunning: false, seconds: 10}
index.js:27 App body --------------
index.js:27 {timerRunning: false, seconds: 10}
index.js:27 *********** runTimer **********
index.js:27 {timerRunning: true, seconds: 0}
index.js:27 App body --------------
index.js:27 {timerRunning: false, seconds: 11}
index.js:27 App body --------------
index.js:27 {timerRunning: false, seconds: 11}
index.js:27 *********** runTimer **********
index.js:27 {timerRunning: true, seconds: 0}
index.js:27 App body --------------
index.js:27 {timerRunning: false, seconds: 12}
index.js:27 App body --------------
index.js:27 {timerRunning: false, seconds: 12}

可以看到,点击Stop后App组件里timerRunning已经是false,但runTimer里还是true,导致定时器持续执行。

问题原因

这是因为React函数组件的闭包特性:

  • 每次组件状态更新(比如setSeconds或setTimerRunning),组件会重新渲染,生成新的函数实例(包括runTimer)。
  • 第一次调用runTimer是在startTimer里,此时的runTimer捕获了当时的timerRunning(true)和seconds(0)。后续递归调用的runTimer都是这个旧的函数实例,它不会感知到组件重新渲染后的新状态。
  • 即使你调用setTimerRunning(false)更新了状态,旧的runTimer闭包里的timerRunning依然是true,所以会继续执行setTimeout。

正确实现方式

要解决这个问题,我们需要让定时器能感知到最新的状态,或者通过清除定时器来停止计时。推荐两种方案:

方案1:使用useRef保存定时器ID,通过清除定时器停止

这种方式更直接,通过控制定时器的生命周期来启停:

import "./styles.css";
import { useState, useRef } from "react";

export default function App() {
  const [timerRunning, setTimerRunning] = useState(false);
  const [seconds, setSeconds] = useState(0);
  const timerRef = useRef(null); // 保存定时器ID

  const startTimer = () => {
    if (!timerRunning) {
      setTimerRunning(true);
      timerRef.current = setInterval(() => {
        setSeconds(prev => prev + 1);
      }, 1000);
    }
  };

  const stopTimer = () => {
    setTimerRunning(false);
    if (timerRef.current) {
      clearInterval(timerRef.current);
      timerRef.current = null;
    }
  };

  const resetTimer = () => {
    stopTimer();
    setSeconds(0);
  };

  return (
    <div className="container">
      <h1>Timer</h1>
      <span> {Math.floor(seconds / 60)} mins </span>
      <span> {seconds % 60} secs</span>
      <div>
        <button onClick={startTimer} disabled={timerRunning}>Start</button>
        <button onClick={stopTimer} disabled={!timerRunning}>Stop</button>
        <button onClick={resetTimer}>Reset</button>
      </div>
    </div>
  );
}
  • 使用useRef保存定时器ID,setInterval替代递归setTimeout更适合定时重复任务。
  • 点击Stop时调用clearInterval清除定时器,同时重置timerRef.current。
  • 给按钮添加disabled属性,避免重复点击Start导致多个定时器同时运行。

方案2:使用useEffect监听timerRunning状态

利用useEffect的依赖项,当timerRunning变化时启动或停止定时器:

import "./styles.css";
import { useState, useEffect, useRef } from "react";

export default function App() {
  const [timerRunning, setTimerRunning] = useState(false);
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);

  useEffect(() => {
    if (timerRunning) {
      intervalRef.current = setInterval(() => {
        setSeconds(prev => prev + 1);
      }, 1000);
    } else {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
        intervalRef.current = null;
      }
    }

    // 组件卸载时清除定时器
    return () => {
      if (intervalRef.current) clearInterval(intervalRef.current);
    };
  }, [timerRunning]); // 依赖timerRunning,状态变化时重新执行

  const startTimer = () => setTimerRunning(true);
  const stopTimer = () => setTimerRunning(false);
  const resetTimer = () => {
    setTimerRunning(false);
    setSeconds(0);
  };

  return (
    <div className="container">
      <h1>Timer</h1>
      <span> {Math.floor(seconds / 60)} mins </span>
      <span> {seconds % 60} secs</span>
      <div>
        <button onClick={startTimer} disabled={timerRunning}>Start</button>
        <button onClick={stopTimer} disabled={!timerRunning}>Stop</button>
        <button onClick={resetTimer}>Reset</button>
      </div>
    </div>
  );
}
  • useEffect的依赖数组包含timerRunning,当它变为true时启动定时器,变为false时清除。
  • 添加组件卸载时的清理函数,防止内存泄漏。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 06:40:35