useIdleTimer.ts
1 // Copyright (c) 2026 VPL Solutions. All rights reserved. 2 // Licensed under the MIT License. See LICENSE for details. 3 4 import { useEffect, useLayoutEffect, useRef, useCallback } from 'react'; 5 6 const ACTIVITY_EVENTS = [ 7 'mousemove', 'mousedown', 'keydown', 'touchstart', 'scroll', 'click', 8 ] as const; 9 10 interface UseIdleTimerOptions { 11 /** Milliseconds of inactivity before onIdle fires. */ 12 timeout: number; 13 /** Called once when the idle threshold is reached. */ 14 onIdle: () => void; 15 /** Set false to disable the timer entirely (e.g. when auth is off). */ 16 enabled?: boolean; 17 } 18 19 /** 20 * Tracks user activity across the window. Fires onIdle after `timeout` ms 21 * of silence. Resets automatically on any activity event. 22 * 23 * Returns a `reset` function to manually restart the countdown. 24 */ 25 export function useIdleTimer({ timeout, onIdle, enabled = true }: UseIdleTimerOptions) { 26 const timer = useRef<ReturnType<typeof setTimeout> | null>(null); 27 // Keep onIdle stable across re-renders without restarting the effect. 28 const onIdleRef = useRef(onIdle); 29 useLayoutEffect(() => { 30 onIdleRef.current = onIdle; 31 }); 32 33 const reset = useCallback(() => { 34 if (timer.current) clearTimeout(timer.current); 35 timer.current = setTimeout(() => onIdleRef.current(), timeout); 36 }, [timeout]); 37 38 useEffect(() => { 39 if (!enabled) return; 40 41 reset(); 42 43 const handleActivity = () => reset(); 44 ACTIVITY_EVENTS.forEach((e) => 45 window.addEventListener(e, handleActivity, { passive: true }), 46 ); 47 48 return () => { 49 if (timer.current) clearTimeout(timer.current); 50 ACTIVITY_EVENTS.forEach((e) => 51 window.removeEventListener(e, handleActivity), 52 ); 53 }; 54 }, [enabled, reset]); 55 56 return { reset }; 57 }