implement custom useTimer hook for improved timer functionality
This commit is contained in:
115
src/hooks/useTimer.js
Normal file
115
src/hooks/useTimer.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Custom hook for timer functionality with high-resolution timing
|
||||
* Fixes timing drift issues by using timestamps instead of interval counting
|
||||
*/
|
||||
const useTimer = (onTimeUpdate = () => {}) => {
|
||||
const [timerState, setTimerState] = useState('stopped') // 'stopped' | 'running' | 'paused'
|
||||
const [time, setTime] = useState(0) // Current time in seconds
|
||||
|
||||
// Refs for timing calculations
|
||||
const startTimeRef = useRef(null)
|
||||
const pausedTimeRef = useRef(0)
|
||||
const intervalRef = useRef(null)
|
||||
const lastNotifiedTimeRef = useRef(0)
|
||||
|
||||
// Update display and notify parent
|
||||
const updateTime = useCallback(() => {
|
||||
if (timerState === 'running' && startTimeRef.current) {
|
||||
const elapsed = Math.floor((Date.now() - startTimeRef.current) / 1000)
|
||||
const newTime = pausedTimeRef.current + elapsed
|
||||
|
||||
setTime(newTime)
|
||||
|
||||
// Only call onTimeUpdate when the second changes to avoid excessive calls
|
||||
if (newTime !== lastNotifiedTimeRef.current) {
|
||||
lastNotifiedTimeRef.current = newTime
|
||||
onTimeUpdate(newTime)
|
||||
}
|
||||
}
|
||||
}, [timerState, onTimeUpdate])
|
||||
|
||||
// Timer effect with high-frequency updates for smooth display
|
||||
useEffect(() => {
|
||||
if (timerState === 'running') {
|
||||
intervalRef.current = setInterval(updateTime, 200)
|
||||
} else {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
}
|
||||
}, [timerState, updateTime])
|
||||
|
||||
// Timer control functions
|
||||
const startTimer = useCallback(() => {
|
||||
const now = Date.now()
|
||||
startTimeRef.current = now
|
||||
pausedTimeRef.current = 0
|
||||
lastNotifiedTimeRef.current = 0
|
||||
setTime(0)
|
||||
setTimerState('running')
|
||||
onTimeUpdate(0)
|
||||
}, [onTimeUpdate])
|
||||
|
||||
const pauseTimer = useCallback(() => {
|
||||
if (timerState === 'running' && startTimeRef.current) {
|
||||
// Calculate and store the elapsed time
|
||||
const elapsed = Math.floor((Date.now() - startTimeRef.current) / 1000)
|
||||
pausedTimeRef.current = pausedTimeRef.current + elapsed
|
||||
setTimerState('paused')
|
||||
}
|
||||
}, [timerState])
|
||||
|
||||
const resumeTimer = useCallback(() => {
|
||||
if (timerState === 'paused') {
|
||||
// Reset start time for resumed session
|
||||
startTimeRef.current = Date.now()
|
||||
setTimerState('running')
|
||||
}
|
||||
}, [timerState])
|
||||
|
||||
const stopTimer = useCallback(() => {
|
||||
setTimerState('stopped')
|
||||
setTime(0)
|
||||
pausedTimeRef.current = 0
|
||||
startTimeRef.current = null
|
||||
lastNotifiedTimeRef.current = 0
|
||||
onTimeUpdate(0)
|
||||
}, [onTimeUpdate])
|
||||
|
||||
const resetTimer = useCallback(() => {
|
||||
stopTimer()
|
||||
}, [stopTimer])
|
||||
|
||||
// Computed properties
|
||||
const isRunning = timerState === 'running'
|
||||
const isPaused = timerState === 'paused'
|
||||
const isStopped = timerState === 'stopped'
|
||||
|
||||
return {
|
||||
// State
|
||||
time,
|
||||
timerState,
|
||||
isRunning,
|
||||
isPaused,
|
||||
isStopped,
|
||||
|
||||
// Actions
|
||||
startTimer,
|
||||
pauseTimer,
|
||||
resumeTimer,
|
||||
stopTimer,
|
||||
resetTimer,
|
||||
}
|
||||
}
|
||||
|
||||
export default useTimer
|
||||
Reference in New Issue
Block a user