import { useEffect, useMemo, useRef } from "preact/hooks"; type Debounced unknown> = & (( ...args: Parameters ) => void) & { cancel: () => void; flush: () => void; pending: () => boolean; }; export default function useDebouncedCallback< T extends (...args: unknown[]) => unknown, >( callback: T, delay: number, options?: { /** Call on the leading edge. Default: false */ leading?: boolean; /** Call on the trailing edge. Default: true */ trailing?: boolean; }, ): Debounced { const callbackRef = useRef(callback); const timerRef = useRef(null); const argsRef = useRef | null>(null); // Always use the latest callback without re-creating the debounced fn useEffect(() => { callbackRef.current = callback; }, [callback]); const leading = !!options?.leading; const trailing = options?.trailing !== false; // default true const debounced = useMemo>(() => { const clear = () => { if (timerRef.current != null) { clearTimeout(timerRef.current); timerRef.current = null; } }; const invoke = () => { const a = argsRef.current; argsRef.current = null; if (a) { callbackRef.current(...a); } }; const fn = ((...args: Parameters) => { const shouldCallLeading = leading && timerRef.current == null; argsRef.current = args; if (timerRef.current != null) clearTimeout(timerRef.current); timerRef.current = globalThis.setTimeout(() => { timerRef.current = null; if (trailing) invoke(); }, delay); if (shouldCallLeading) { // Leading edge call happens immediately invoke(); } }) as Debounced; fn.cancel = () => { argsRef.current = null; clear(); }; fn.flush = () => { if (timerRef.current != null) { clear(); invoke(); } }; fn.pending = () => timerRef.current != null; return fn; // Recreate only if timing/edge behavior changes }, [delay, leading, trailing]); // Cancel on unmount useEffect(() => () => debounced.cancel(), [debounced]); return debounced; }