Last active
December 2, 2021 02:59
-
-
Save RoyalIcing/9ec00d41ee4c7c574d606c98b52f7866 to your computer and use it in GitHub Desktop.
Debouncing with logical clocks in React
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Examples
Debouncing a prop passed to you, waiting for 300 milliseconds:
Notifying analytics when a piece of state changes.
Firing an effect after a button has stopped being clicked for 300 milliseconds: