Effects
useDebounceCallback
Debounces a callback, with cancel, flush and isPending.
Demo
live · imported from hookli
- keystrokes
- 0
- callback runs
- 0
- last searched
- —
import { useState } from "react";
import { useDebounceCallback } from "hookli";
export function Demo() {
const [query, setQuery] = useState("");
const search = useDebounceCallback((q: string) => {
console.log("searching", q);
}, 600);
return (
<input
value={query}
onChange={(e) => {
setQuery(e.target.value);
search(e.target.value);
}}
/>
);
}Usage
import { useState } from "react";
import { useDebounceCallback } from "hookli";
export function Demo() {
const [query, setQuery] = useState("");
const search = useDebounceCallback((q: string) => {
console.log("searching", q);
}, 600);
return (
<input
value={query}
onChange={(e) => {
setQuery(e.target.value);
search(e.target.value);
}}
/>
);
}API
useDebounceCallback<Args, R>(fn: (...args: Args) => R, delayMs?: number, options?: DebounceOptions): DebouncedState<Args, R>Parameters
| Name | Type | Default | Description |
|---|---|---|---|
| fn | (...args: Args) => R | — | The function to debounce. The returned function keeps a stable identity across renders but always invokes the latest fn. |
| delayMs | number | 500 | Milliseconds to wait after the last call before fn runs. |
| options | DebounceOptions | {} | Leading/trailing edge and maxWait behaviour. See the table below. |
Returns
| Name | Type | Description |
|---|---|---|
| debounced | DebouncedState<Args, R> | A debounced version of fn with a stable identity, carrying cancel, flush and isPending. Any pending call is cancelled on unmount. |
DebounceOptions
Controls how the debounced invocation is scheduled.
| Name | Type | Description |
|---|---|---|
| leading | boolean | Invoke on the leading edge — run once immediately on the first call of a burst. |
| trailing | boolean | Invoke on the trailing edge — run after the burst settles. |
| maxWait | number | The maximum time the callback may be delayed before it is forced to run, even during a continuous burst. |
DebouncedState<Args, R>
The debounced function, plus manual control methods.
| Name | Type | Description |
|---|---|---|
| (...args) | (...args: Args) => R | undefined | Calling it schedules an invocation and returns the last computed result — undefined before the first run. |
| cancel | () => void | Cancels any pending trailing invocation. |
| flush | () => R | undefined | Immediately runs any pending invocation and returns its result. |
| isPending | () => boolean | Whether a trailing invocation is currently scheduled. |
Hook
The implementation, straight from hookli. Copy it, or install the package.
import { useEffect, useMemo, useRef } from "react";
import { useEventCallback } from "../use-event-callback/use-event-callback";
import { useUnmount } from "../use-unmount/use-unmount";
export interface DebounceOptions {
/** Invoke on the leading edge of the timeout. Defaults to `false`. */
leading?: boolean;
/** Invoke on the trailing edge of the timeout. Defaults to `true`. */
trailing?: boolean;
/** Maximum time the callback may be delayed before it is forced to run. */
maxWait?: number;
}
export interface DebouncedState<Args extends unknown[], R> {
(...args: Args): R | undefined;
cancel: () => void;
flush: () => R | undefined;
isPending: () => boolean;
}
export const useDebounceCallback = <Args extends unknown[], R>(
fn: (...args: Args) => R,
delayMs = 500,
options: DebounceOptions = {},
): DebouncedState<Args, R> => {
const timeoutId = useRef<ReturnType<typeof setTimeout>>();
const maxTimeoutId = useRef<ReturnType<typeof setTimeout>>();
const lastArgs = useRef<Args>();
const lastResult = useRef<R>();
const lastInvokeTime = useRef(0);
const { leading = false, trailing = true, maxWait } = options;
const latestFn = useEventCallback(fn);
useEffect(() => {
lastInvokeTime.current = 0;
}, [delayMs, leading, trailing, maxWait]);
const debounced = useMemo(() => {
const invoke = (): R | undefined => {
const args = lastArgs.current;
if (!args) return undefined;
lastArgs.current = undefined;
lastInvokeTime.current = Date.now();
lastResult.current = latestFn(...args);
return lastResult.current;
};
const clearTimers = () => {
if (timeoutId.current !== undefined) {
clearTimeout(timeoutId.current);
timeoutId.current = undefined;
}
if (maxTimeoutId.current !== undefined) {
clearTimeout(maxTimeoutId.current);
maxTimeoutId.current = undefined;
}
};
const trailingEdge = () => {
clearTimers();
if (trailing && lastArgs.current) invoke();
else lastArgs.current = undefined;
};
const state = Object.assign(
(...args: Args): R | undefined => {
lastArgs.current = args;
const isFirstCall =
timeoutId.current === undefined && maxTimeoutId.current === undefined;
if (timeoutId.current !== undefined) clearTimeout(timeoutId.current);
if (leading && isFirstCall) invoke();
timeoutId.current = setTimeout(trailingEdge, delayMs);
if (maxWait !== undefined && maxTimeoutId.current === undefined) {
maxTimeoutId.current = setTimeout(() => {
clearTimers();
if (lastArgs.current) invoke();
}, maxWait);
}
return lastResult.current;
},
{
cancel: () => {
clearTimers();
lastArgs.current = undefined;
},
flush: (): R | undefined => {
if (timeoutId.current === undefined) return lastResult.current;
clearTimers();
return lastArgs.current ? invoke() : lastResult.current;
},
isPending: () =>
timeoutId.current !== undefined && lastArgs.current !== undefined,
},
);
return state;
}, [delayMs, leading, trailing, maxWait, latestFn]);
useUnmount(() => {
debounced.cancel();
});
return debounced;
};