|
| 1 | +import { useCallback, useEffect, useRef } from 'react'; |
| 2 | + |
| 3 | +export interface UseDebounceOptions { |
| 4 | + /** |
| 5 | + * - **EN** Whether to execute at the start of the wait period. Default is `false`. |
| 6 | + * - **CN** 是否在等待周期开始时执行,默认值为 `false` |
| 7 | + */ |
| 8 | + leading?: boolean; |
| 9 | + /** |
| 10 | + * - **EN** Regular debounce interval in milliseconds. Default is `0`, meaning no debounce. |
| 11 | + * - **CN** 常规防抖间隔 (ms),默认值为 `0`, 表示不进行防抖 |
| 12 | + */ |
| 13 | + wait?: number; |
| 14 | + /** |
| 15 | + * - **EN** Maximum wait time in milliseconds. Default is `0`, meaning no maximum wait. |
| 16 | + * - **CN** 最大等待时间 (ms),默认值为 `0`, 表示不限制最大等待时间 |
| 17 | + */ |
| 18 | + maxWait?: number; |
| 19 | +} |
| 20 | +/** |
| 21 | + * - **EN** Debounce Hook with dual trigger mechanisms: |
| 22 | + * |
| 23 | + * 1. Traditional debounce: Executes after a specified interval without new calls. |
| 24 | + * 2. Max wait: Forces execution after exceeding a specified maximum wait time. |
| 25 | + * - **CN** 防抖 Hook:具有两种触发机制: |
| 26 | + * |
| 27 | + * 1. 传统防抖:等待指定时间内无新调用后执行 |
| 28 | + * 2. 最大等待:超过指定最大等待时间后强制执行 |
| 29 | + * |
| 30 | + * @param fn The function to debounce | 需要防抖的函数 |
| 31 | + * @param deps Dependency array, re-creates the debounced function when dependencies change | |
| 32 | + * 依赖项数组,当依赖变化时重新创建防抖函数 |
| 33 | + * @param options Configuration options | 配置选项 |
| 34 | + * |
| 35 | + * @returns The debounced function | 防抖处理后的函数 |
| 36 | + */ |
| 37 | +function useDebounce<T extends (...args: any[]) => unknown>( |
| 38 | + fn: T, |
| 39 | + deps: React.DependencyList, |
| 40 | + options: UseDebounceOptions = {} |
| 41 | +): (...args: Parameters<T>) => void { |
| 42 | + const { wait = 0, maxWait = 0, leading = false } = options; |
| 43 | + const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
| 44 | + const fnRef = useRef<T>(fn); |
| 45 | + const lastExecutedTimeRef = useRef<number>(0); // The last execution timestamp |
| 46 | + const lastArgsRef = useRef<unknown[]>([]); // The last call arguments |
| 47 | + |
| 48 | + // Update the latest function reference |
| 49 | + useEffect(() => { |
| 50 | + fnRef.current = fn; |
| 51 | + }, [fn]); |
| 52 | + |
| 53 | + // Cleanup on unmount |
| 54 | + useEffect(() => { |
| 55 | + return () => { |
| 56 | + if (timeoutRef.current) { |
| 57 | + clearTimeout(timeoutRef.current); |
| 58 | + timeoutRef.current = null; |
| 59 | + } |
| 60 | + }; |
| 61 | + }, []); |
| 62 | + |
| 63 | + // The actual execution function |
| 64 | + const executeFunction = useCallback(() => { |
| 65 | + timeoutRef.current = null; |
| 66 | + lastExecutedTimeRef.current = Date.now(); |
| 67 | + fnRef.current(...lastArgsRef.current); |
| 68 | + }, []); |
| 69 | + |
| 70 | + // The debounced function |
| 71 | + return useCallback( |
| 72 | + (...args: Parameters<T>) => { |
| 73 | + const now = Date.now(); |
| 74 | + lastArgsRef.current = args; |
| 75 | + |
| 76 | + // If leading is true and it's the first call, execute immediately |
| 77 | + if (leading && timeoutRef.current === null && now - lastExecutedTimeRef.current >= wait) { |
| 78 | + executeFunction(); |
| 79 | + return; |
| 80 | + } |
| 81 | + |
| 82 | + // 1. Clear the existing timer |
| 83 | + if (timeoutRef.current !== null) { |
| 84 | + clearTimeout(timeoutRef.current); |
| 85 | + timeoutRef.current = null; |
| 86 | + } |
| 87 | + |
| 88 | + // 2. Check if the maximum wait time has been exceeded, if so, execute immediately |
| 89 | + if (maxWait > 0 && now - lastExecutedTimeRef.current >= maxWait) { |
| 90 | + executeFunction(); |
| 91 | + return; |
| 92 | + } |
| 93 | + |
| 94 | + // 3. Set a new debounce timer |
| 95 | + timeoutRef.current = setTimeout(executeFunction, wait); |
| 96 | + }, |
| 97 | + // eslint-disable-next-line @tiny-codes/react-hooks/exhaustive-deps |
| 98 | + [wait, maxWait, executeFunction, leading, ...deps] |
| 99 | + ); |
| 100 | +} |
| 101 | + |
| 102 | +export default useDebounce; |
0 commit comments