Files
memorium/lib/hooks/useDebouncedCallback.ts
2025-11-03 00:03:27 +01:00

94 lines
2.2 KiB
TypeScript

import { useEffect, useMemo, useRef } from "preact/hooks";
type Debounced<T extends (...args: unknown[]) => unknown> =
& ((
...args: Parameters<T>
) => 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<T> {
const callbackRef = useRef(callback);
const timerRef = useRef<number | null>(null);
const argsRef = useRef<Parameters<T> | 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<Debounced<T>>(() => {
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<T>) => {
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<T>;
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;
}