组件路由时倒计时计时器停止渲染且重置问题求助
问题解决:路由切换后倒计时计时器重置且定时器泄漏的问题
问题根源
- 组件卸载时未清除定时器:路由切换到Timer组件时,Countdown组件被卸载,但
setInterval创建的定时器仍在后台运行,造成内存泄漏。 - 闭包捕获旧值:
updateTime函数里的time是组件初始渲染时捕获的闭包值,后续setTime更新后,这个值不会同步,导致计算的剩余时间错误。 - 状态未持久化:组件卸载后state丢失,重新挂载时回到初始值600。
解决方案
方案1:修复定时器泄漏 + 闭包问题 + 本地存储持久化
修改Countdown组件,通过useRef保存定时器ID,组件卸载时清除;用函数式更新获取最新state;同时用localStorage持久化剩余时间:
import { useState, useRef, useEffect } from "react"; import { Link } from "react-router-dom"; const Countdown = () => { // 从localStorage读取初始值,没有则用600 const [time, setTime] = useState(() => { const saved = localStorage.getItem("countdownTime"); return saved ? parseInt(saved, 10) : 600; }); const intervalRef = useRef(null); const startTimeRef = useRef(null); // 组件卸载时清除定时器 useEffect(() => { return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } }; }, []); // 每次time变化时保存到localStorage useEffect(() => { localStorage.setItem("countdownTime", time.toString()); }, [time]); const startTimer = () => { if (intervalRef.current) return; // 防止重复启动 startTimeRef.current = Date.now(); intervalRef.current = setInterval(() => { // 用函数式更新获取最新的time值 setTime(prevTime => { const elapsed = ((Date.now() - startTimeRef.current) / 1000) | 0; const updatedTime = prevTime - elapsed; // 重置定时器并停止计时 if (updatedTime <= 0) { clearInterval(intervalRef.current); intervalRef.current = null; return 0; } startTimeRef.current = Date.now(); // 重置起始时间,避免累计误差 return updatedTime; }); console.log("Set Interval running"); }, 1000); }; const minutes = (time / 60) | 0; const seconds = time % 60 | 0; return ( <> <h3> {minutes.toString().padStart(2, "0")}:{seconds.toString().padStart(2, "0")} </h3> <p>先启动计时器 👇</p> <button onClick={startTimer} disabled={intervalRef.current !== null}> {intervalRef.current ? "运行中" : "START"} </button> <p>启动后跳转到其他组件 👇</p> <Link to="timer"> <button>前往Timer组件</button> </Link> </> ); }; export default Countdown;
方案2:状态提升到App组件(更适合跨组件共享状态)
如果需要在多个组件中共享计时器状态,可以把time和定时器控制逻辑放到App组件,通过props传递给Countdown:
// App组件 import { Route, Routes, useState, useRef, useEffect } from "react-router-dom"; import Countdown from "./components/Countdown"; import Timer from "./components/Timer"; const App = () => { const [time, setTime] = useState(600); const intervalRef = useRef(null); const startTimeRef = useRef(null); useEffect(() => { return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, []); const startTimer = () => { if (intervalRef.current) return; startTimeRef.current = Date.now(); intervalRef.current = setInterval(() => { setTime(prev => { const elapsed = ((Date.now() - startTimeRef.current)/1000)|0; const updated = prev - elapsed; if (updated <=0) { clearInterval(intervalRef.current); intervalRef.current = null; return 0; } startTimeRef.current = Date.now(); return updated; }); },1000); }; return ( <> <Routes> <Route path="/" element={<Countdown time={time} startTimer={startTimer} isRunning={!!intervalRef.current} />} /> <Route path="timer" element={<Timer />} /> </Routes> </> ); }; export default App;
修改Countdown组件,接收props:
import { Link } from "react-router-dom"; const Countdown = ({ time, startTimer, isRunning }) => { const minutes = (time / 60) | 0; const seconds = time % 60 | 0; return ( <> <h3> {minutes.toString().padStart(2, "0")}:{seconds.toString().padStart(2, "0")} </h3> <p>先启动计时器 👇</p> <button onClick={startTimer} disabled={isRunning}> {isRunning ? "运行中" : "START"} </button> <p>启动后跳转到其他组件 👇</p> <Link to="timer"> <button>前往Timer组件</button> </Link> </> ); }; export default Countdown;
关键改进点
- 清除定时器:利用
useEffect的清理函数,在组件卸载时调用clearInterval,避免内存泄漏。 - 函数式更新state:
setTime(prevTime => ...)确保每次获取的都是最新的状态值,解决闭包问题。 - 状态持久化:通过localStorage或状态提升,保证组件重新挂载时不会丢失之前的计时状态。
- 防止重复启动:通过
intervalRef判断定时器是否已在运行,避免多次点击创建多个定时器。
内容的提问来源于stack exchange,提问作者krishnakanta
相关产品推荐
相关产品推荐

