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

React定时器调用clearInterval()后重启而非暂停,求排查

问题描述

我希望通过切换按钮实现定时器的启停,同时添加重置按钮,但每次点击停止后定时器都会自动重启。请问我的代码哪里出现了问题?

相关代码

timer.js

import React, { useState, useImperativeHandle, forwardRef } from 'react'

const Timer = forwardRef((props, ref) => {
  // 注:这里需要用ref来追踪setInterval,以便在需要时停止它
  let interval = null;

  const [total, setTotal] = useState(3540);      

  useImperativeHandle(ref, () => ({
    startTimerWrapper() {
      interval = setInterval(() => {
        setTotal(total => total + 1)
      },1000)

      console.log(interval);
  
      return () => clearInterval(this.interval)
    },
    closeTimerWRapper(){
      clearInterval(interval);
      console.log("cleared " + this.interval);
      setTotal(0);
    }
  }));

  return (
    <>
      <h1 >
        <span>{("0" + Math.floor(total / 3600)).slice(-2)}:</span>
        <span>{("0" + Math.floor((total % 3600) / 60)).slice(-2)}:</span>
        <span>{("0" + ~~(total % 60)).slice(-2)}</span>
      </h1>
    </> 
  )
})

export default Timer;

App.js

function App() {
  const [started, setStarted] = React.useState(false);
  const timerRef = useRef(null);

  const onToggle = () => {    
    let id = null;
    setStarted(!started); 

    if(!started){
      id = timerRef.current.startTimerWrapper();
      console.log("1");
    } else {
      timerRef.current.closeTimerWRapper();
      console.log("0 " + id);
    }
  }

  return (
    <div className="App" style={{ padding: 40 }} >        
      <Card className="main" style={style.main}>
        <CardContent>
          <Stack
            direction="row"
            justifyContent="center"            
            spacing={2}        
          >
            <Item>
              <StyledToggleButton                    
                value="false"
                selected={started}
                onChange={() => {
                  onToggle();          
                }}
              >
                <PlayCircleOutlineIcon sx={{display: started ? 'none' : 'block'}}/>
                <StopIcon sx={{display: started ? 'block' : 'none' }}/>
              </StyledToggleButton>
            </Item>             
          </Stack>
          <div justifycontent="center" display="flex"><Timer ref={timerRef} ></Timer></div>
        </CardContent>
      </Card>
    </div>
  );
}

export default App;
问题原因与修复方案

核心问题

  1. 定时器ID未持久化存储:Timer组件里的interval是普通变量,组件每次因setTotal更新状态而重渲染时,这个变量会被重置为null。调用closeTimerWRapper时,拿到的是新的null值,无法清除之前启动的定时器,导致旧定时器一直运行,看起来像是“停止后自动重启”。
  2. 清理函数无效:startTimerWrapper里返回的清理函数没有被触发,且this.interval指向错误,无法正确清除定时器。

修复步骤

1. 用useRef存储定时器ID

把普通的let interval = null替换为useRef,确保定时器ID在组件生命周期内保持不变:

// 在Timer组件导入useRef
import React, { useState, useImperativeHandle, forwardRef, useRef } from 'react'

// 替换原来的let interval = null;
const intervalRef = useRef(null);

2. 修改启停与重置方法

更新startTimerWrapper、closeTimerWRapper,新增resetTimer方法,使用intervalRef.current操作定时器:

useImperativeHandle(ref, () => ({
  startTimerWrapper() {
    // 启动前先清除旧定时器,避免重复创建
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
    }
    intervalRef.current = setInterval(() => {
      setTotal(total => total + 1);
    }, 1000);
  },
  closeTimerWRapper(){
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null; // 清除后重置ref
      setTotal(0);
    }
  },
  resetTimer() {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
    setTotal(0);
  }
}));

3. 优化App组件的切换逻辑

setStarted是异步操作,用函数式更新确保拿到最新状态:

const onToggle = () => {    
  setStarted(prevStarted => {
    if (!prevStarted) {
      timerRef.current.startTimerWrapper();
    } else {
      timerRef.current.closeTimerWRapper();
    }
    return !prevStarted;
  });
}

4. 添加重置按钮

在App组件的Stack中加入重置按钮:

<Item>
  <Button onClick={() => timerRef.current.resetTimer()}>
    重置
  </Button>
</Item>

完整修复后的Timer组件

import React, { useState, useImperativeHandle, forwardRef, useRef } from 'react'

const Timer = forwardRef((props, ref) => {
  const intervalRef = useRef(null);
  const [total, setTotal] = useState(3540);      

  useImperativeHandle(ref, () => ({
    startTimerWrapper() {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
      intervalRef.current = setInterval(() => {
        setTotal(total => total + 1);
      }, 1000);
    },
    closeTimerWRapper(){
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
        intervalRef.current = null;
        setTotal(0);
      }
    },
    resetTimer() {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
        intervalRef.current = null;
      }
      setTotal(0);
    }
  }));

  return (
    <>
      <h1 >
        <span>{("0" + Math.floor(total / 3600)).slice(-2)}:</span>
        <span>{("0" + Math.floor((total % 3600) / 60)).slice(-2)}:</span>
        <span>{("0" + ~~(total % 60)).slice(-2)}</span>
      </h1>
    </> 
  )
})

export default Timer;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 13:45:15