use-interval.ts
1 import { useContext, useEffect, useRef, useState } from 'react' 2 import { ClockContext } from '../components/ClockContext.js' 3 4 /** 5 * Returns the clock time, updating at the given interval. 6 * Subscribes as non-keepAlive — won't keep the clock alive on its own, 7 * but updates whenever a keepAlive subscriber (e.g. the spinner) 8 * is driving the clock. 9 * 10 * Use this to drive pure time-based computations (shimmer position, 11 * frame index) from the shared clock. 12 */ 13 export function useAnimationTimer(intervalMs: number): number { 14 const clock = useContext(ClockContext) 15 const [time, setTime] = useState(() => clock?.now() ?? 0) 16 17 useEffect(() => { 18 if (!clock) return 19 20 let lastUpdate = clock.now() 21 22 const onChange = (): void => { 23 const now = clock.now() 24 if (now - lastUpdate >= intervalMs) { 25 lastUpdate = now 26 setTime(now) 27 } 28 } 29 30 return clock.subscribe(onChange, false) 31 }, [clock, intervalMs]) 32 33 return time 34 } 35 36 /** 37 * Interval hook backed by the shared Clock. 38 * 39 * Unlike `useInterval` from `usehooks-ts` (which creates its own setInterval), 40 * this piggybacks on the single shared clock so all timers consolidate into 41 * one wake-up. Pass `null` for intervalMs to pause. 42 */ 43 export function useInterval( 44 callback: () => void, 45 intervalMs: number | null, 46 ): void { 47 const callbackRef = useRef(callback) 48 callbackRef.current = callback 49 50 const clock = useContext(ClockContext) 51 52 useEffect(() => { 53 if (!clock || intervalMs === null) return 54 55 let lastUpdate = clock.now() 56 57 const onChange = (): void => { 58 const now = clock.now() 59 if (now - lastUpdate >= intervalMs) { 60 lastUpdate = now 61 callbackRef.current() 62 } 63 } 64 65 return clock.subscribe(onChange, false) 66 }, [clock, intervalMs]) 67 }