UNPKG

4.68 kBJavaScriptView Raw
1import { useMemo, useRef } from 'react';
2import useTimeout from './useTimeout';
3import useEventCallback from './useEventCallback';
4import useWillUnmount from './useWillUnmount';
5
6/**
7 * Creates a debounced function that will invoke the input function after the
8 * specified wait.
9 *
10 * > Heads up! debounced functions are not pure since they are called in a timeout
11 * > Don't call them inside render.
12 *
13 * @param fn a function that will be debounced
14 * @param waitOrOptions a wait in milliseconds or a debounce configuration
15 */
16
17/**
18 * Creates a debounced function that will invoke the input function after the
19 * specified wait.
20 *
21 * > Heads up! debounced functions are not pure since they are called in a timeout
22 * > Don't call them inside render.
23 *
24 * @param fn a function that will be debounced
25 * @param waitOrOptions a wait in milliseconds or a debounce configuration
26 */
27
28function useDebouncedCallback(fn, waitOrOptions) {
29 const lastCallTimeRef = useRef(null);
30 const lastInvokeTimeRef = useRef(0);
31 const returnValueRef = useRef();
32 const isTimerSetRef = useRef(false);
33 const lastArgsRef = useRef(null);
34 // Use any to bypass type issue with setTimeout.
35 const timerRef = useRef(0);
36 const handleCallback = useEventCallback(fn);
37 const {
38 wait,
39 maxWait,
40 leading = false,
41 trailing = true
42 } = typeof waitOrOptions === 'number' ? {
43 wait: waitOrOptions
44 } : waitOrOptions;
45 const timeout = useTimeout();
46 useWillUnmount(() => {
47 clearTimeout(timerRef.current);
48 isTimerSetRef.current = false;
49 });
50 return useMemo(() => {
51 const hasMaxWait = !!maxWait;
52 function leadingEdge(time) {
53 // Reset any `maxWait` timer.
54 lastInvokeTimeRef.current = time;
55
56 // Start the timer for the trailing edge.
57 isTimerSetRef.current = true;
58 timeout.set(timerExpired, wait);
59 if (!leading) {
60 return returnValueRef.current;
61 }
62 return invokeFunc(time);
63 }
64 function trailingEdge(time) {
65 isTimerSetRef.current = false;
66
67 // Only invoke if we have `lastArgs` which means `func` has been
68 // debounced at least once.
69 if (trailing && lastArgsRef.current) {
70 return invokeFunc(time);
71 }
72 lastArgsRef.current = null;
73 return returnValueRef.current;
74 }
75 function timerExpired() {
76 var _lastCallTimeRef$curr;
77 var time = Date.now();
78 if (shouldInvoke(time)) {
79 return trailingEdge(time);
80 }
81 const timeSinceLastCall = time - ((_lastCallTimeRef$curr = lastCallTimeRef.current) != null ? _lastCallTimeRef$curr : 0);
82 const timeSinceLastInvoke = time - lastInvokeTimeRef.current;
83 const timeWaiting = wait - timeSinceLastCall;
84
85 // Restart the timer.
86 timeout.set(timerExpired, hasMaxWait ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting);
87 }
88 function invokeFunc(time) {
89 var _lastArgsRef$current;
90 const args = (_lastArgsRef$current = lastArgsRef.current) != null ? _lastArgsRef$current : [];
91 lastArgsRef.current = null;
92 lastInvokeTimeRef.current = time;
93 const retValue = handleCallback(...args);
94 returnValueRef.current = retValue;
95 return retValue;
96 }
97 function shouldInvoke(time) {
98 var _lastCallTimeRef$curr2;
99 const timeSinceLastCall = time - ((_lastCallTimeRef$curr2 = lastCallTimeRef.current) != null ? _lastCallTimeRef$curr2 : 0);
100 const timeSinceLastInvoke = time - lastInvokeTimeRef.current;
101
102 // Either this is the first call, activity has stopped and we're at the
103 // trailing edge, the system time has gone backwards and we're treating
104 // it as the trailing edge, or we've hit the `maxWait` limit.
105 return lastCallTimeRef.current === null || timeSinceLastCall >= wait || timeSinceLastCall < 0 || hasMaxWait && timeSinceLastInvoke >= maxWait;
106 }
107 return (...args) => {
108 const time = Date.now();
109 const isInvoking = shouldInvoke(time);
110 lastArgsRef.current = args;
111 lastCallTimeRef.current = time;
112 if (isInvoking) {
113 if (!isTimerSetRef.current) {
114 return leadingEdge(lastCallTimeRef.current);
115 }
116 if (hasMaxWait) {
117 // Handle invocations in a tight loop.
118 isTimerSetRef.current = true;
119 timerRef.current = setTimeout(timerExpired, wait);
120 return invokeFunc(lastCallTimeRef.current);
121 }
122 }
123 if (!isTimerSetRef.current) {
124 isTimerSetRef.current = true;
125 timerRef.current = setTimeout(timerExpired, wait);
126 }
127 return returnValueRef.current;
128 };
129 }, [handleCallback, wait, maxWait, leading, trailing]);
130}
131export default useDebouncedCallback;
\No newline at end of file