diff --git a/.changeset/icy-impalas-try.md b/.changeset/icy-impalas-try.md new file mode 100644 index 0000000..9cc16cd --- /dev/null +++ b/.changeset/icy-impalas-try.md @@ -0,0 +1,5 @@ +--- +"tiny-hooks": minor +--- + +Add new `useIdle` hook diff --git a/src/index.ts b/src/index.ts index 387ceaa..8ce0bde 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ export { useCounter } from "./useCounter"; export { useDebounce } from "./useDebounce"; export { useEventListener } from "./useEventListener"; export { useHover } from "./useHover"; +export { useIdle } from "./useIdle"; export { useIsClient } from "./useIsClient"; export { useIsMounted } from "./useIsMounted"; export { useLocalStorage } from "./useLocalStorage"; diff --git a/src/useIdle/index.ts b/src/useIdle/index.ts new file mode 100644 index 0000000..aaa5909 --- /dev/null +++ b/src/useIdle/index.ts @@ -0,0 +1,2 @@ +export type { UseIdleOptions, UseIdleReturn } from "./types"; +export { useIdle } from "./useIdle"; diff --git a/src/useIdle/types.ts b/src/useIdle/types.ts new file mode 100644 index 0000000..c486647 --- /dev/null +++ b/src/useIdle/types.ts @@ -0,0 +1,10 @@ +export interface UseIdleOptions { + timeout?: number; + onIdle?: () => void; + events?: string[]; +} + +export interface UseIdleReturn { + isIdle: boolean; + idleTime: number; +} diff --git a/src/useIdle/useIdle.ts b/src/useIdle/useIdle.ts new file mode 100644 index 0000000..ee2ab55 --- /dev/null +++ b/src/useIdle/useIdle.ts @@ -0,0 +1,52 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import type { UseIdleOptions, UseIdleReturn } from "./types.ts"; + +const defaultEvents = [ + "mousemove", + "mousedown", + "keydown", + "scroll", + "touchstart", + "touchmove", +]; + +export function useIdle({ + timeout = 300_000, // 5 minutes + onIdle, + events = defaultEvents, +}: UseIdleOptions = {}): UseIdleReturn { + const [isIdle, setIsIdle] = useState(false); + const [lastActive, setLastActive] = useState(Date.now()); + const timer = useRef(null); + + const resetTimer = useCallback(() => { + if (timer.current) clearTimeout(timer.current); + + setIsIdle(false); + setLastActive(Date.now()); + + timer.current = window.setTimeout(() => { + setIsIdle(true); + onIdle?.(); + }, timeout); + }, [timeout, onIdle]); + + useEffect(() => { + events.forEach((event) => { + window.addEventListener(event, resetTimer, true); + }); + + resetTimer(); + + return () => { + if (timer.current) clearTimeout(timer.current); + events.forEach((event) => { + window.removeEventListener(event, resetTimer, true); + }); + }; + }, [resetTimer, events]); + + const idleTime = Date.now() - lastActive; + + return { isIdle, idleTime }; +}