diff --git a/packages/usehooks-ts/CHANGELOG.md b/packages/usehooks-ts/CHANGELOG.md index c4336b4b..32560804 100644 --- a/packages/usehooks-ts/CHANGELOG.md +++ b/packages/usehooks-ts/CHANGELOG.md @@ -95,7 +95,6 @@ ### Minor Changes - 87a5141: Improve `useOnClickOutside`: - - Prevent handling callback when clicking on a not connected element (#374 by @hooriza) - Add support to accept multiple references - Add support for touch events in addition to mouse events diff --git a/packages/usehooks-ts/src/index.ts b/packages/usehooks-ts/src/index.ts index 9577bd16..b27b3f29 100644 --- a/packages/usehooks-ts/src/index.ts +++ b/packages/usehooks-ts/src/index.ts @@ -5,6 +5,7 @@ export * from './useCountdown' export * from './useCounter' export * from './useDarkMode' export * from './useDebounceCallback' +export * from './useDebounceEffect' export * from './useDebounceValue' export * from './useDocumentTitle' export * from './useEventCallback' diff --git a/packages/usehooks-ts/src/useDebounceEffect/index.ts b/packages/usehooks-ts/src/useDebounceEffect/index.ts new file mode 100644 index 00000000..862a00d7 --- /dev/null +++ b/packages/usehooks-ts/src/useDebounceEffect/index.ts @@ -0,0 +1 @@ +export * from './useDebounceEffect' diff --git a/packages/usehooks-ts/src/useDebounceEffect/useDebounceEffect.demo.tsx b/packages/usehooks-ts/src/useDebounceEffect/useDebounceEffect.demo.tsx new file mode 100644 index 00000000..55b15100 --- /dev/null +++ b/packages/usehooks-ts/src/useDebounceEffect/useDebounceEffect.demo.tsx @@ -0,0 +1,17 @@ +import { useState } from 'react'; +import { useDebounceEffect } from './useDebounceEffect' + +export default function Component() { + const [value, setValue] = useState(0); + + useDebounceEffect(() => { + console.log('Debounced effect executed with value:', value); + }, [value], 1000); + + return
+

useDebounceEffect Demo

+

Current value: {value}

+ + +
+} diff --git a/packages/usehooks-ts/src/useDebounceEffect/useDebounceEffect.md b/packages/usehooks-ts/src/useDebounceEffect/useDebounceEffect.md new file mode 100644 index 00000000..a15a2dd6 --- /dev/null +++ b/packages/usehooks-ts/src/useDebounceEffect/useDebounceEffect.md @@ -0,0 +1,11 @@ +Creates a debounced version of an effect function. + +### Parameters + +- `effect`: The effect to be debounced. +- `dependencies`: An array of dependencies that will trigger the effect when changed. +- `delay`: The debounce delay in milliseconds. + +### Returns + +A debounced version of the original effect along with control functions. diff --git a/packages/usehooks-ts/src/useDebounceEffect/useDebounceEffect.test.ts b/packages/usehooks-ts/src/useDebounceEffect/useDebounceEffect.test.ts new file mode 100644 index 00000000..9df6214d --- /dev/null +++ b/packages/usehooks-ts/src/useDebounceEffect/useDebounceEffect.test.ts @@ -0,0 +1,76 @@ +import { renderHook } from '@testing-library/react'; + +import { useDebounceEffect } from './useDebounceEffect'; + +vitest.useFakeTimers(); + +describe('useDebounceEffect()', () => { + const DELAY = 500; + + it('should execute the effect after the specified delay.', () => { + // Given + const effect = vitest.fn(); + const dependencies = [1]; + + // When + renderHook(() => useDebounceEffect(effect, dependencies, DELAY)); + vitest.advanceTimersByTime(DELAY); + + // Then + expect(effect).toHaveBeenCalledTimes(1); + }); + + it('should not execute the effect if dependencies change before the delay expires.', () => { + // Given + const effect = vitest.fn(); + let dependencies = [1]; + + // When + const { rerender } = renderHook(() => useDebounceEffect(effect, dependencies, DELAY)); + + dependencies = [2]; + rerender(); + + vitest.advanceTimersByTime(DELAY); + + // Then + expect(effect).toHaveBeenCalledTimes(1); + }); + + it('should execute the effect again after dependencies change after the daily.', () => { + // Given + const effect = vitest.fn(); + let dependencies = [1]; + + // When + const { rerender } = renderHook(() => useDebounceEffect(effect, dependencies, DELAY)); + + vitest.advanceTimersByTime(DELAY); + + expect(effect).toHaveBeenCalledTimes(1); + + dependencies = [2]; + rerender(); + + vitest.advanceTimersByTime(DELAY); + + // Then + expect(effect).toHaveBeenCalledTimes(2); + }); + + it('should clear the timeout when unmounted.', () => { + // Given + const effect = vitest.fn(); + const dependencies = [1]; + + // When + const { unmount } = renderHook(() => useDebounceEffect(effect, dependencies, DELAY)); + + unmount(); + + vitest.advanceTimersByTime(DELAY); + + // Then + expect(effect).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/usehooks-ts/src/useDebounceEffect/useDebounceEffect.ts b/packages/usehooks-ts/src/useDebounceEffect/useDebounceEffect.ts new file mode 100644 index 00000000..19cd5ec5 --- /dev/null +++ b/packages/usehooks-ts/src/useDebounceEffect/useDebounceEffect.ts @@ -0,0 +1,41 @@ +import { useEffect, useRef } from 'react'; + +/** + * Custom hooks to debounce an effect. + * This hook will delay the execution of the effect until after the specified delay has passed since the last time the dependencies changed. + * It is similar to `useEffect`, but it adds a debounce mechanism. + * @param effect The effect to run after the debounce delay. + * @param dependencies An array of dependencies that will trigger the effect when changed. + * @param delay The debounce delay in milliseconds. + * @returns void + * @public + * @see https://usehooks-ts.com/react-hook/use-debounce-effect + * @example + * ```tsx + * useDebounceEffect(() => { + * console.log('Effect executed after debounce delay'); + * }, [dependency1, dependency2], 500); + * ``` + */ +export function useDebounceEffect( + effect: (...args: Array) => void, + dependencies: Array, + delay: number +) { + const ref = useRef(); + + useEffect(() => { + if (ref.current) { + clearTimeout(ref.current); + } + ref.current = setTimeout(() => { + effect(); + clearTimeout(ref.current); + }, delay); + return () => { + if (ref.current) { + clearTimeout(ref.current); + } + } + }, [effect, ...dependencies, delay]); +}