@react-hookz/web
Version:
React hooks done right, for browser and SSR.
64 lines (63 loc) • 2.38 kB
JavaScript
import { useEffect, useMemo, useRef } from 'react';
import { useUnmountEffect } from '../useUnmountEffect/index.js';
/**
* Makes passed function debounced, otherwise acts like `useCallback`.
*
* @param callback Function that will be debounced.
* @param deps Dependencies list when to update callback. It also replaces invoked
* callback for scheduled debounced invocations.
* @param delay Debounce delay.
* @param maxWait The maximum time `callback` is allowed to be delayed before
* it's invoked. 0 means no max wait.
*/
export function useDebouncedCallback(callback, deps, delay, maxWait = 0) {
const timeout = useRef(undefined);
const waitTimeout = useRef(undefined);
const cb = useRef(callback);
const lastCall = useRef(undefined);
const clear = () => {
if (timeout.current) {
clearTimeout(timeout.current);
timeout.current = undefined;
}
if (waitTimeout.current) {
clearTimeout(waitTimeout.current);
waitTimeout.current = undefined;
}
};
// Cancel scheduled execution on unmount
useUnmountEffect(clear);
useEffect(() => {
cb.current = callback;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return useMemo(() => {
const execute = () => {
clear();
if (!lastCall.current) {
return;
}
const context = lastCall.current;
lastCall.current = undefined;
cb.current.apply(context.this, context.args);
};
const wrapped = function (...args) {
if (timeout.current) {
clearTimeout(timeout.current);
}
lastCall.current = { args, this: this };
// Plan regular execution
timeout.current = setTimeout(execute, delay);
// Plan maxWait execution if required
if (maxWait > 0 && !waitTimeout.current) {
waitTimeout.current = setTimeout(execute, maxWait);
}
};
Object.defineProperties(wrapped, {
length: { value: callback.length },
name: { value: `${callback.name || 'anonymous'}__debounced__${delay}` },
});
return wrapped;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [delay, maxWait, ...deps]);
}