如何在React JS中实现页面切换/刷新后可恢复的累加计时器
累加计时器状态持久化实现方案
方案选型参考
根据需求场景选存储介质即可,useContext本身不具备持久化能力,仅用于跨组件共享状态,需要搭配Web Storage使用:
sessionStorage:单标签页生命周期内生效,刷新、同标签页路由跳转状态不丢失,关闭标签页自动清空,适合临时计时场景localStorage:持久化存储(除非手动清除浏览器缓存),支持跨标签页状态同步,适合需要关闭浏览器后下次打开仍能续算的场景
核心实现逻辑
不要只持久化累计时长,否则计时器运行中跳页/刷新时,离开页面的这段时间不会被计入,会出现时间不准的问题。需要持久化三个核心字段:
- 已经累计的计时毫秒数
- 计时器当前运行状态(运行/暂停)
- 最后一次更新状态的时间戳
初始化读取状态时做特殊处理:如果上次离开时计时器处于运行状态,用当前时间戳减去存储的最后更新时间戳,得到离开期间的时间差,加到累计时长上,即可实现无缝续算。每次状态变更、页面卸载、组件卸载时同步写入存储,就能保证状态不丢。
完整可运行代码(React技术栈)
以下是集成了sessionStorage持久化的计时器组件代码,JS项目直接删除TypeScript类型定义即可使用:
import { useState, useEffect, useRef, useCallback } from 'react'; // 存储键名,避免和站点其他存储字段冲突 const STORAGE_KEY = 'ACCUMULATIVE_TIMER_STATE'; type PersistedTimerState = { elapsed: number; // 累计毫秒数 isRunning: boolean; // 运行状态 lastTimestamp: number; // 最后一次更新的时间戳 }; // 初始化时读取存储状态,自动计算离线期间的时间差 const getInitState = (): PersistedTimerState => { const storedVal = sessionStorage.getItem(STORAGE_KEY); // 无存储记录时返回初始状态 if (!storedVal) return { elapsed: 0, isRunning: false, lastTimestamp: Date.now() }; try { const parsed = JSON.parse(storedVal) as PersistedTimerState; // 上次离开时计时器在运行,补上离开期间的时长 if (parsed.isRunning) { const now = Date.now(); return { ...parsed, elapsed: parsed.elapsed + (now - parsed.lastTimestamp), lastTimestamp: now }; } return parsed; } catch (err) { // 存储内容损坏/格式错误时返回初始状态,避免页面崩溃 return { elapsed: 0, isRunning: false, lastTimestamp: Date.now() }; } }; const PersistentTimer = () => { const [timerState, setTimerState] = useState<PersistedTimerState>(getInitState); const timerRef = useRef<number | null>(null); // 统一持久化方法 const saveState = useCallback((state: PersistedTimerState) => { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state)); }, []); // 启动 const handleStart = useCallback(() => { setTimerState(prev => { const newState = { ...prev, isRunning: true, lastTimestamp: Date.now() }; saveState(newState); return newState; }); }, [saveState]); // 暂停 const handlePause = useCallback(() => { setTimerState(prev => { const now = Date.now(); const newState = { ...prev, isRunning: false, elapsed: prev.elapsed + (now - prev.lastTimestamp), lastTimestamp: now }; saveState(newState); return newState; }); }, [saveState]); // 重置 const handleReset = useCallback(() => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } const newState = { elapsed: 0, isRunning: false, lastTimestamp: Date.now() }; saveState(newState); setTimerState(newState); }, [saveState]); // 运行中计时逻辑 useEffect(() => { if (timerState.isRunning) { // 100ms更新一次UI,保证显示流畅 timerRef.current = window.setInterval(() => { setTimerState(prev => { const now = Date.now(); return { ...prev, elapsed: prev.elapsed + (now - prev.lastTimestamp), lastTimestamp: now }; }); }, 100); } return () => { if (timerRef.current) clearInterval(timerRef.current); }; }, [timerState.isRunning]); // 页面刷新/关闭/路由跳走前自动存状态 useEffect(() => { const handleBeforeUnload = () => saveState(timerState); window.addEventListener('beforeunload', handleBeforeUnload); return () => { window.removeEventListener('beforeunload', handleBeforeUnload); // 组件卸载(路由跳转)时存一次 saveState(timerState); }; }, [timerState, saveState]); // 毫秒转时分秒展示 const formatDisplay = (ms: number) => { const totalSec = Math.floor(ms / 1000); const h = String(Math.floor(totalSec / 3600)).padStart(2, '0'); const m = String(Math.floor((totalSec % 3600) / 60)).padStart(2, '0'); const s = String(totalSec % 60).padStart(2, '0'); return `${h}:${m}:${s}`; }; return ( <div className="timer-container"> <div className="time-show">{formatDisplay(timerState.elapsed)}</div> <div className="btn-group"> {timerState.isRunning ? <button onClick={handlePause}>暂停</button> : <button onClick={handleStart}>启动</button> } <button onClick={handleReset}>重置</button> </div> </div> ); }; export default PersistentTimer;
跨组件共享扩展(useContext实现)
如果需要在其他页面/组件中获取计时器状态、控制计时器,可以包一层Context做全局共享,避免重复初始化逻辑:
import { createContext, useContext, useMemo } from 'react'; type TimerContextValue = { elapsed: number; isRunning: boolean; start: () => void; pause: () => void; reset: () => void; }; const TimerContext = createContext<TimerContextValue | null>(null); // 全局Provider,包裹在路由根组件外层即可 export const TimerProvider = ({ children }: { children: React.ReactNode }) => { // 此处将上面PersistentTimer组件中的状态、方法逻辑移入即可 // 省略重复逻辑... const contextValue = useMemo(() => ({ elapsed: timerState.elapsed, isRunning: timerState.isRunning, start: handleStart, pause: handlePause, reset: handleReset }), [timerState.elapsed, timerState.isRunning, handleStart, handlePause, handleReset]); return <TimerContext.Provider value={contextValue}>{children}</TimerContext.Provider>; }; // 自定义调用hook export const useTimer = () => { const ctx = useContext(TimerContext); if (!ctx) throw new Error('useTimer 必须在 TimerProvider 内部使用'); return ctx; };
其他组件直接调用const { elapsed, isRunning } = useTimer()即可获取状态,不需要重复对接存储逻辑。
常见优化点
- 需要关闭浏览器仍保留计时状态、跨标签页同步,直接将代码中所有
sessionStorage替换为localStorage即可,可额外增加storage事件监听实现多标签页状态自动同步 - 存储写入不需要和UI更新同频,仅在启动、暂停、重置、页面卸载几个节点写入即可,避免频繁IO影响性能
- 无痕模式下部分浏览器会禁用Web Storage,可在初始化时加判断,降级为内存存储不影响基础使用
内容的提问来源于stack exchange,提问作者AniMi
相关产品推荐
相关产品推荐

