@react-hookz/web
Version:
React hooks done right, for browser and SSR.
53 lines (52 loc) • 2.15 kB
JavaScript
import { useMemo, useRef } from 'react';
import { useUnmountEffect } from '../useUnmountEffect/index.js';
/**
* Makes passed function throttled, otherwise acts like `useCallback`.
*
* @param callback Function that will be throttled.
* @param deps Dependencies list when to update callback.
* @param delay Throttle delay.
* @param noTrailing If `noTrailing` is true, callback will only execute every
* `delay` milliseconds, otherwise, callback will be executed one final time
* after the last throttled-function call.
*/
export function useThrottledCallback(callback, deps, delay, noTrailing = false) {
const timeout = useRef(undefined);
const lastCall = useRef(undefined);
useUnmountEffect(() => {
if (timeout.current) {
clearTimeout(timeout.current);
timeout.current = undefined;
}
});
return useMemo(() => {
const execute = (context, args) => {
lastCall.current = undefined;
callback.apply(context, args);
timeout.current = setTimeout(() => {
timeout.current = undefined;
// If trailing execution is not disabled - call callback with last
// received arguments and context
if (!noTrailing && lastCall.current) {
execute(lastCall.current.this, lastCall.current.args);
lastCall.current = undefined;
}
}, delay);
};
const wrapped = function (...args) {
if (timeout.current) {
// If we cant execute callback immediately - save its arguments and
// context to execute it when delay is passed
lastCall.current = { args, this: this };
return;
}
execute(this, args);
};
Object.defineProperties(wrapped, {
length: { value: callback.length },
name: { value: `${callback.name || 'anonymous'}__throttled__${delay}` },
});
return wrapped;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [delay, noTrailing, ...deps]);
}