All files / src/hooks useDebounce.ts

100% Statements 148/148
100% Branches 11/11
100% Functions 3/3
100% Lines 148/148

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 1491x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 26x 26x 26x 26x 19x 19x 7x 19x 19x 19x 19x 19x 19x 26x 26x 26x 26x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 10x 10x 10x 10x 10x 10x 10x 8x 8x 2x 2x 8x 8x 8x 4x 8x 8x 8x 10x 10x 10x 10x 10x 10x 4x 4x 10x 10x 10x 10x 10x  
/**
 * Debounce Hooks
 *
 * React hooks for debouncing values and callbacks.
 * Useful for search inputs, form validation, and API calls.
 *
 * @module hooks/useDebounce
 */
 
import { useState, useEffect } from "react";
 
/**
 * Hook that debounces a value
 *
 * Returns a debounced version of the provided value that only updates
 * after the specified delay has passed without any new changes.
 *
 * @typeParam T - The type of the value being debounced
 * @param value - The value to debounce
 * @param delay - The delay in milliseconds (default: 500ms)
 * @returns The debounced value
 *
 * @example
 * ```tsx
 * // Debounce search input
 * function SearchBar() {
 *   const [searchTerm, setSearchTerm] = useState('');
 *   const debouncedSearch = useDebounce(searchTerm, 300);
 *
 *   useEffect(() => {
 *     if (debouncedSearch) {
 *       // Only fires 300ms after user stops typing
 *       fetchSearchResults(debouncedSearch);
 *     }
 *   }, [debouncedSearch]);
 *
 *   return <input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} />;
 * }
 * ```
 *
 * @example
 * ```tsx
 * // Debounce form validation
 * const [email, setEmail] = useState('');
 * const debouncedEmail = useDebounce(email, 500);
 *
 * useEffect(() => {
 *   if (debouncedEmail) {
 *     validateEmail(debouncedEmail);
 *   }
 * }, [debouncedEmail]);
 * ```
 */
export function useDebounce<T>(value: T, delay: number = 500): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);
 
  useEffect(() => {
    // Set up the timeout to update debounced value after delay
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
 
    // Cleanup function to cancel the timeout if value changes before delay
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);
 
  return debouncedValue;
}
 
/**
 * Hook for debounced callbacks
 *
 * Returns a debounced version of a callback function that only executes
 * after the specified delay has passed without any new calls.
 *
 * @typeParam T - The function type
 * @param callback - The callback function to debounce
 * @param delay - The delay in milliseconds (default: 500ms)
 * @returns The debounced callback function
 *
 * @example
 * ```tsx
 * // Debounce API calls on user input
 * function AutosaveForm() {
 *   const [content, setContent] = useState('');
 *
 *   const debouncedSave = useDebouncedCallback(
 *     async (text: string) => {
 *       await saveToServer(text);
 *     },
 *     1000
 *   );
 *
 *   const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
 *     setContent(e.target.value);
 *     debouncedSave(e.target.value);
 *   };
 *
 *   return <textarea value={content} onChange={handleChange} />;
 * }
 * ```
 *
 * @example
 * ```tsx
 * // Debounce window resize handler
 * const handleResize = useDebouncedCallback(() => {
 *   recalculateLayout();
 * }, 200);
 *
 * useEffect(() => {
 *   window.addEventListener('resize', handleResize);
 *   return () => window.removeEventListener('resize', handleResize);
 * }, [handleResize]);
 * ```
 */
export function useDebouncedCallback<T extends (...args: never[]) => unknown>(
  callback: T,
  delay: number = 500
): T {
  const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | null>(null);
 
  const debouncedCallback = ((...args: Parameters<T>) => {
    // Clear existing timeout
    if (timeoutId) {
      clearTimeout(timeoutId);
    }
 
    // Set new timeout
    const newTimeoutId = setTimeout(() => {
      callback(...args);
    }, delay);
 
    setTimeoutId(newTimeoutId);
  }) as T;
 
  // Cleanup on unmount
  useEffect(() => {
    return () => {
      if (timeoutId) {
        clearTimeout(timeoutId);
      }
    };
  }, [timeoutId]);
 
  return debouncedCallback;
}