94 lines
2.2 KiB
TypeScript
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;
|
|
}
|