Skip to content

Instantly share code, notes, and snippets.

@grantglidewell
Created April 24, 2020 17:08
Show Gist options
  • Save grantglidewell/9ebb44eac6143b9bf0f261e6dba9b1a0 to your computer and use it in GitHub Desktop.
Save grantglidewell/9ebb44eac6143b9bf0f261e6dba9b1a0 to your computer and use it in GitHub Desktop.
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useRef, useCallback, useEffect } from 'react';
export default function useDebouncedCallback<T extends (...args: any[]) => any>(
callback: T,
delay: number,
options: { maxWait?: number; leading?: boolean; trailing?: boolean } = {},
): [T, () => void, () => void] {
const maxWait = options.maxWait;
const maxWaitHandler = useRef<NodeJS.Timeout | null>(null);
const maxWaitArgs: { current: any[] } = useRef([]);
const leading = options.leading;
const trailing = options.trailing === undefined ? true : options.trailing;
const leadingCall = useRef(false);
const functionTimeoutHandler = useRef<NodeJS.Timeout | null>(null);
const isComponentUnmounted: { current: boolean } = useRef(false);
const debouncedFunction = useRef(callback);
debouncedFunction.current = callback;
const cancelDebouncedCallback: () => void = useCallback(() => {
functionTimeoutHandler.current &&
clearTimeout(functionTimeoutHandler.current);
maxWaitHandler.current && clearTimeout(maxWaitHandler.current);
maxWaitHandler.current = null;
maxWaitArgs.current = [];
functionTimeoutHandler.current = null;
leadingCall.current = false;
}, []);
useEffect(
() => (): void => {
// we use flag, as we allow to call callPending outside the hook
isComponentUnmounted.current = true;
},
[],
);
const debouncedCallback = useCallback(
(...args) => {
maxWaitArgs.current = args;
functionTimeoutHandler.current &&
clearTimeout(functionTimeoutHandler.current);
if (leadingCall.current) {
leadingCall.current = false;
}
if (!functionTimeoutHandler.current && leading && !leadingCall.current) {
debouncedFunction.current(...args);
leadingCall.current = true;
}
functionTimeoutHandler.current = setTimeout(() => {
let shouldCallFunction = true;
if (leading && leadingCall.current) {
shouldCallFunction = false;
}
cancelDebouncedCallback();
if (!isComponentUnmounted.current && trailing && shouldCallFunction) {
debouncedFunction.current(...args);
}
}, delay);
if (maxWait && !maxWaitHandler.current && trailing) {
maxWaitHandler.current = setTimeout(() => {
const args = maxWaitArgs.current;
cancelDebouncedCallback();
if (!isComponentUnmounted.current) {
debouncedFunction.current.apply(null, args);
}
}, maxWait);
}
},
[maxWait, delay, cancelDebouncedCallback, leading, trailing],
);
const callPending = useCallback(() => {
// Call pending callback only if we have anything in our queue
if (!functionTimeoutHandler.current) {
return;
}
debouncedFunction.current.apply(null, maxWaitArgs.current);
cancelDebouncedCallback();
}, [cancelDebouncedCallback]);
// At the moment, we use 3 args array so that we save backward compatibility
return [debouncedCallback as T, cancelDebouncedCallback, callPending];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment