Skip to content

Commit 8d10578

Browse files
committed
refactor: Create debounce utility hooks
1 parent 92006aa commit 8d10578

File tree

1 file changed

+87
-0
lines changed

1 file changed

+87
-0
lines changed

site/src/hooks/debounce.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { useCallback, useEffect, useRef, useState } from "react";
2+
3+
type useDebouncedFunctionReturn<Args extends unknown[]> = Readonly<{
4+
debounced: (...args: Args) => void;
5+
6+
// Mainly here to make interfacing with useEffect cleanup functions easier
7+
cancelDebounce: () => void;
8+
}>;
9+
10+
/**
11+
* Creates a debounce function that is resilient to React re-renders, as well as
12+
* a function for canceling a pending debounce.
13+
*
14+
* The returned-out functions will maintain the same memory references, but the
15+
* debounce function will always "see" the most recent versions of the arguments
16+
* passed into the hook, and use them accordingly.
17+
*
18+
* If the debounce time changes while a callback has been queued to fire, the
19+
* callback will be canceled completely. You will need to restart the debounce
20+
* process by calling debounced again.
21+
*/
22+
export function useDebouncedFunction<
23+
// Parameterizing on the args instead of the whole callback function type to
24+
// avoid type contra-variance issues; want to avoid need for type assertions
25+
Args extends unknown[] = unknown[],
26+
>(
27+
callback: (...args: Args) => void | Promise<void>,
28+
debounceTimeMs: number,
29+
): useDebouncedFunctionReturn<Args> {
30+
const timeoutIdRef = useRef<number | null>(null);
31+
const cancelDebounce = useCallback(() => {
32+
// Clearing timeout because, even though hot-swapping the timeout value
33+
// while keeping any active debounced functions running was possible, it
34+
// seemed like it'd be ripe for bugs. Can redesign the logic if that ends up
35+
// becoming an actual need down the line.
36+
if (timeoutIdRef.current !== null) {
37+
window.clearTimeout(timeoutIdRef.current);
38+
}
39+
40+
timeoutIdRef.current = null;
41+
}, []);
42+
43+
const debounceTimeRef = useRef(debounceTimeMs);
44+
useEffect(() => {
45+
cancelDebounce();
46+
debounceTimeRef.current = debounceTimeMs;
47+
}, [cancelDebounce, debounceTimeMs]);
48+
49+
const callbackRef = useRef(callback);
50+
useEffect(() => {
51+
callbackRef.current = callback;
52+
}, [callback]);
53+
54+
// Returned-out function will always be synchronous, even if the callback arg
55+
// is async. Seemed dicey to try awaiting a genericized operation that can and
56+
// will likely be canceled repeatedly
57+
const debounced = useCallback(
58+
(...args: Args): void => {
59+
cancelDebounce();
60+
61+
timeoutIdRef.current = window.setTimeout(
62+
() => void callbackRef.current(...args),
63+
debounceTimeRef.current,
64+
);
65+
},
66+
[cancelDebounce],
67+
);
68+
69+
return { debounced, cancelDebounce } as const;
70+
}
71+
72+
export function useDebouncedValue<T = unknown>(
73+
value: T,
74+
debounceTimeMs: number,
75+
): T {
76+
const [debouncedValue, setDebouncedValue] = useState(value);
77+
78+
useEffect(() => {
79+
const timeoutId = window.setTimeout(() => {
80+
setDebouncedValue(value);
81+
}, debounceTimeMs);
82+
83+
return () => window.clearTimeout(timeoutId);
84+
}, [value, debounceTimeMs]);
85+
86+
return debouncedValue;
87+
}

0 commit comments

Comments
 (0)