Skip to content

Instantly share code, notes, and snippets.

@RoyalIcing
Last active December 2, 2021 02:59
Show Gist options
  • Save RoyalIcing/9ec00d41ee4c7c574d606c98b52f7866 to your computer and use it in GitHub Desktop.
Save RoyalIcing/9ec00d41ee4c7c574d606c98b52f7866 to your computer and use it in GitHub Desktop.
Debouncing with logical clocks in React
import { DispatchWithoutAction, useEffect, useMemo, useReducer } from 'react';
import type { DependencyList, EffectCallback } from 'react';
/**
* A logical clock.
*
* @returns a tuple with the current clock value, and a stable function that advances the clock.
*/
export function useTicker(): [number, DispatchWithoutAction] {
return useReducer(n => n + 1, 0);
}
/**
* A debounced logical clock. Calling the returned callback schedules the clock to advance. It only advances once the provided duration has passed with no further calls. If it is called again, the scheduled time restarts.
*
* @param {number} duration How to long wait to advance the clock.
* @returns a tuple with the current clock value, and a stable function that advances the clock.
*/
export function useDebouncedTicker(duration: number): readonly [number, EffectCallback] {
const [count, advance] = useTicker();
const callback = useMemo(() => {
let timeout: null | ReturnType<typeof setTimeout> = null;
function clear() {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
}
return () => {
clear();
timeout = setTimeout(advance, duration);
return clear;
};
}, [duration, advance]);
return [count, callback];
}
/**
* A debounced `useEffect`.
*
* @param effect The effect to run once `deps` stop changing and `duration` has passed.
* @param duration How long to wait before firing the effect.
* @param deps The array of changes to observe.
*/
export function useDebouncedEffect(effect: EffectCallback, duration: number, deps: DependencyList): void {
const [tick, scheduleAdvance] = useDebouncedTicker(duration);
useEffect(scheduleAdvance, deps); // When our deps change, notify our debouncer.
useEffect(effect, [tick]); // When our debouncer finishes, run our effect.
}
/**
* A debounced `useMemo`.
*
* @param factory The calculation to memoize.
* @param duration How long to wait after changes before recalculating the value.
* @param deps The array of changes to observe.
* @returns The calculated value.
*/
export function useDebouncedMemo<T>(factory: () => T, duration: number, deps: DependencyList): T {
const [tick, scheduleAdvance] = useDebouncedTicker(duration);
useEffect(scheduleAdvance, deps); // When our deps change, notify our debouncer.
return useMemo(factory, [tick]); // When our debouncer finishes, invalidate our memo.
}
@RoyalIcing
Copy link
Author

RoyalIcing commented Nov 12, 2021

Examples

Debouncing a prop passed to you, waiting for 300 milliseconds:

const debouncedValue = useDebouncedMemo(() => props.value, 300, [props.value]);

Notifying analytics when a piece of state changes.

const [state, setState] = useState();

useDebouncedEffect(() => {
  trackAnalyticsEvent('some-category', state);
}, 300, [state]);

Firing an effect after a button has stopped being clicked for 300 milliseconds:

const [tick, dispatch] = useDebouncedTicker(300);
useEffect(() => {
 // My action I want debounced.
}, [tick]);

return <button onClick={dispatch}>Click me</button>;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment