diff --git a/packages/raystack/hooks/index.tsx b/packages/raystack/hooks/index.tsx index 8b3da246..580ae6cc 100644 --- a/packages/raystack/hooks/index.tsx +++ b/packages/raystack/hooks/index.tsx @@ -1,3 +1,4 @@ export { useCopyToClipboard } from './useCopyToClipboard'; export { useMouse } from './useMouse'; export { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; +export { useDebouncedState } from './useDebouncedState'; diff --git a/packages/raystack/hooks/useDebouncedState.tsx b/packages/raystack/hooks/useDebouncedState.tsx new file mode 100644 index 00000000..125119b0 --- /dev/null +++ b/packages/raystack/hooks/useDebouncedState.tsx @@ -0,0 +1,68 @@ +import { + SetStateAction, + useCallback, + useEffect, + useRef, + useState +} from 'react'; + +export interface UseDebouncedStateOptions { + /** + * If `true`, the state will be updated immediately on the first call (leading edge). + * Subsequent calls within the delay period will be debounced. + * @default false + */ + leading?: boolean; +} + +export type UseDebouncedStateReturnValue = [ + T, + (newValue: SetStateAction) => void +]; + +/** + * A hook that debounces the state update. + * @param defaultValue - The default value of the state. + * @param delay - The delay time in milliseconds. + * @param options - The options for the hook. + * @returns A tuple containing the current value and the debounced set value function. + * + * @example + * const [value, setValue] = useDebouncedState('Hello', 1000); + + * @example + * const [value, setValue] = useDebouncedState('Hello', 1000, { leading: true }); + */ +export function useDebouncedState( + defaultValue: T, + delay: number, + options: UseDebouncedStateOptions = { leading: false } +): UseDebouncedStateReturnValue { + const [value, setValue] = useState(defaultValue); + const timeoutRef = useRef(null); + const leadingRef = useRef(true); + + const clearTimeout = useCallback( + () => window.clearTimeout(timeoutRef.current!), + [] + ); + useEffect(() => clearTimeout, [clearTimeout]); + + const debouncedSetValue = useCallback( + (newValue: SetStateAction) => { + clearTimeout(); + if (leadingRef.current && options.leading) { + setValue(newValue); + } else { + timeoutRef.current = window.setTimeout(() => { + leadingRef.current = true; + setValue(newValue); + }, delay); + } + leadingRef.current = false; + }, + [options.leading, clearTimeout, delay] + ); + + return [value, debouncedSetValue] as const; +}