# useDebounceCallback > Debounces a callback, with cancel, flush and isPending. - Category: Effects - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-debounce-callback ## Signature ```ts useDebounceCallback(fn: (...args: Args) => R, delayMs?: number, options?: DebounceOptions): DebouncedState ``` ## Parameters - `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` (default: `500`) — Milliseconds to wait after the last call before fn runs. - `options`: `DebounceOptions` (default: `{}`) — Leading/trailing edge and maxWait behaviour. See the table below. ## Returns - `debounced`: `DebouncedState` — A debounced version of fn with a stable identity, carrying cancel, flush and isPending. Any pending call is cancelled on unmount. ## Types ### DebounceOptions Controls how the debounced invocation is scheduled. - `leading`: `boolean` (default: `false`) — Invoke on the leading edge — run once immediately on the first call of a burst. - `trailing`: `boolean` (default: `true`) — 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 The debounced function, plus manual control methods. - `(...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. ## Usage ```tsx 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 ( { setQuery(e.target.value); search(e.target.value); }} /> ); } ``` ## Source `src/hooks/use-debounce-callback/use-debounce-callback.ts` ```ts 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: Args): R | undefined; cancel: () => void; flush: () => R | undefined; isPending: () => boolean; } export const useDebounceCallback = ( fn: (...args: Args) => R, delayMs = 500, options: DebounceOptions = {}, ): DebouncedState => { const timeoutId = useRef>(); const maxTimeoutId = useRef>(); const lastArgs = useRef(); const lastResult = useRef(); 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; }; ```