React中页面刷新/关闭后仍运行的Boss倒计时计时器问题
React Boss倒计时计时器修复方案
问题现状
- 页面刷新后,计时器冻结在刷新瞬间的时间点,无法继续倒计时
- 其他设备访问站点时,计时器重置为代码设置的默认时间
- 使用localStorage存储计时数据后,数据虽能保留,但倒计时功能停止工作
问题根源
- 刷新后定时器未自动启动:原代码中
isRunning默认值为false,刷新后不会自动触发定时器,导致倒计时中断。 - 存储逻辑错误:直接存储剩余秒数而非目标结束时间戳,刷新后无法计算当前时刻到结束时刻的真实剩余时间,导致时间偏差。
- 跨设备同步限制:localStorage是本地浏览器存储,无法在多设备间共享数据,因此其他设备访问会使用默认时间(此问题需后端支持,本文先解决本地持久化问题)。
修复后的代码
import React, { useState, useEffect } from "react"; import "./App.css"; const BossTimer = ({ boss }) => { const { name, race, elements, bossRespawnTime, image, raceImage } = boss; const timerKey = `timer_end_${name}`; // 初始化剩余时间和运行状态 const [remainingTime, setRemainingTime] = useState(() => { const storedEndTime = parseInt(localStorage.getItem(timerKey)); if (storedEndTime) { // 计算当前到结束时间的剩余秒数,最小为0 const diff = Math.floor((storedEndTime - Date.now()) / 1000); return Math.max(diff, 0); } // 无存储数据则用默认时间 return bossRespawnTime; }); const [isRunning, setIsRunning] = useState(() => { const storedEndTime = parseInt(localStorage.getItem(timerKey)); // 如果有存储的结束时间且未到点,自动启动 return storedEndTime && storedEndTime > Date.now(); }); useEffect(() => { let interval; if (isRunning && remainingTime > 0) { interval = setInterval(() => { setRemainingTime(prev => { const newTime = prev - 1; // 更新存储的结束时间戳 const newEndTime = Date.now() + newTime * 1000; localStorage.setItem(timerKey, newEndTime.toString()); return newTime; }); }, 1000); } else if (remainingTime <= 0) { // 倒计时结束,停止并清除存储 setIsRunning(false); localStorage.removeItem(timerKey); } return () => clearInterval(interval); }, [isRunning, remainingTime, timerKey]); const startTimer = () => { // 启动时设置结束时间戳 const endTime = Date.now() + bossRespawnTime * 1000; localStorage.setItem(timerKey, endTime.toString()); setRemainingTime(bossRespawnTime); setIsRunning(true); }; const resetTimer = () => { setIsRunning(false); setRemainingTime(bossRespawnTime); localStorage.removeItem(timerKey); }; const formatTime = (timeInSeconds) => { const hours = Math.floor(Math.abs(timeInSeconds) / 3600); const minutes = Math.floor((Math.abs(timeInSeconds) % 3600) / 60); const seconds = Math.abs(timeInSeconds) % 60; return `${timeInSeconds < 0 ? "-" : ""}${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`; }; return ( <tr> <td> <div className="boss-info"> <img src={image} alt={`${name} 头像`} width="50" height="50" /> <span className="boss-name">{name}</span> </div> </td> <td> <img src={raceImage} alt={`${race} 种族图标`} width="30" height="30" /> <span className="boss-race">{race}</span> </td> <td> <div className="element-image"> {elements.map((element, index) => ( <img key={index} src={element} alt={`属性图标 ${index + 1}`} width="20" height="20" /> ))} </div> </td> <td> <div className="timer"> <span>{formatTime(remainingTime)}</span> <div className="timer-buttons"> <button onClick={startTimer} disabled={isRunning}>Start</button> <button onClick={resetTimer}>Restart</button> </div> </div> </td> </tr> ); }; function App() { const bosses = [ { name: "Архон", bossRespawnTime: 4 * 60 * 60 + 20 * 60, image: "URL_OF_IMAGE_1", race: "Undead", elements: ["URL_OF_ELEMENT_2_1"], raceImage: "URL_OF_RACE_IMAGE_2" }, { name: "Boss 2", bossRespawnTime: 3 * 60 * 60 + 15 * 60, image: "URL_OF_IMAGE_2", race: "Undead", elements: ["URL_OF_ELEMENT_2_1"], raceImage: "URL_OF_RACE_IMAGE_2" }, // 可添加更多Boss数据 ]; return ( <div className="App"> <h1>Boss 倒计时</h1> <table> <thead> <tr> <th>Boss信息</th> <th>种族</th> <th>属性</th> <th>计时器/操作</th> </tr> </thead> <tbody> {bosses.map((boss, index) => ( <BossTimer key={index} boss={boss} /> ))} </tbody> </table> </div> ); } export default App;
关键修改说明
- 存储结束时间戳:不再存储剩余秒数,改为存储倒计时结束的时间戳(
Date.now() + 剩余秒数*1000),刷新后可通过当前时间与结束时间的差值计算真实剩余时间。 - 自动启动定时器:初始化时判断存储的结束时间是否未到期,自动设置
isRunning为true,无需手动点击Start按钮。 - 倒计时结束处理:当剩余时间≤0时,自动停止定时器并清除localStorage数据。
- 修复语法错误:修正了原代码中
bosses数组内的多余逗号。
跨设备同步说明
如果需要实现多设备间计时器同步,仅靠前端localStorage无法完成,需要后端存储每个Boss的倒计时结束时间,前端从后端拉取最新数据并同步更新,同时前端操作(启动/重置)也要通知后端更新数据。
内容的提问来源于stack exchange,提问作者Irakli Gvelesiani
相关产品推荐
相关产品推荐

