diff --git a/packages/core/src/bundle/hooks/state.js b/packages/core/src/bundle/hooks/state.js index 10c9c9f2..4b12c445 100644 --- a/packages/core/src/bundle/hooks/state.js +++ b/packages/core/src/bundle/hooks/state.js @@ -12,6 +12,7 @@ export * from './useList/useList'; // storage export * from './useLocalStorage/useLocalStorage'; export * from './useMap/useMap'; +export * from './useObject/useObject'; export * from './useOffsetPagination/useOffsetPagination'; export * from './useQuery/useQuery'; export * from './useQueue/useQueue'; diff --git a/packages/core/src/bundle/hooks/useDebounceEffect/useDebounceEffect.js b/packages/core/src/bundle/hooks/useDebounceEffect/useDebounceEffect.js new file mode 100644 index 00000000..3feb6dc7 --- /dev/null +++ b/packages/core/src/bundle/hooks/useDebounceEffect/useDebounceEffect.js @@ -0,0 +1,24 @@ +import { useEffect } from 'react'; +import { useDebounceCallback } from '../utilities'; +/** + * @name useDebounceEffect + * @description - Hook that runs an effect function with a debounce delay + * @category Utilities + * + * @template Params The type of the parameters passed to the effect + * @param {() => void | Promise} effect The effect function to execute with debounce + * @param {React.DependencyList} dependency The dependency array for the effect + * @param {number} delay The debounce delay in milliseconds before running the effect + * @returns {void} + * + * @example + * useDebounceEffect(() => { + * console.log("Effect called with delay"); + * }, [value], 500); + */ +export const useDebounceEffect = (effect, dependency, delay) => { + const debouncedCallback = useDebounceCallback(effect, delay); + useEffect(() => { + debouncedCallback(); + }, [debouncedCallback, ...dependency]); +}; diff --git a/packages/core/src/bundle/hooks/utilities.js b/packages/core/src/bundle/hooks/utilities.js index f65ed3f4..f3e27abc 100644 --- a/packages/core/src/bundle/hooks/utilities.js +++ b/packages/core/src/bundle/hooks/utilities.js @@ -2,6 +2,7 @@ export * from './useConst/useConst'; // timing export * from './useDebounceCallback/useDebounceCallback'; +export * from './useDebounceEffect/useDebounceEffect'; export * from './useDebounceValue/useDebounceValue'; export * from './useEvent/useEvent'; export * from './useLastChanged/useLastChanged'; diff --git a/packages/core/src/hooks/useDebounceEffect/useDebounceEffect.demo.tsx b/packages/core/src/hooks/useDebounceEffect/useDebounceEffect.demo.tsx new file mode 100644 index 00000000..4514ccda --- /dev/null +++ b/packages/core/src/hooks/useDebounceEffect/useDebounceEffect.demo.tsx @@ -0,0 +1,32 @@ +import { useState } from 'react'; + +import { useDebounceEffect } from './useDebounceEffect'; + +const Demo = () => { + const [value, setValue] = useState(''); + const [debouncedValue, setDebouncedValue] = useState(''); + + useDebounceEffect( + () => { + setDebouncedValue(value); + }, + [value], + 500 + ); + + return ( + <> + setValue(e.target.value)} + placeholder='Type something...' + /> +

Value: {value}

+

Debounced value: {debouncedValue}

+ + ); +}; + +export default Demo; diff --git a/packages/core/src/hooks/useDebounceEffect/useDebounceEffect.test.ts b/packages/core/src/hooks/useDebounceEffect/useDebounceEffect.test.ts new file mode 100644 index 00000000..bce69e27 --- /dev/null +++ b/packages/core/src/hooks/useDebounceEffect/useDebounceEffect.test.ts @@ -0,0 +1,67 @@ +import { act, renderHook } from '@testing-library/react'; +import { vi } from 'vitest'; + +import { useDebounceEffect } from './useDebounceEffect'; + +beforeEach(vi.useFakeTimers); + +afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); +}); + +it('Should run the effect after the delay', () => { + const effect = vi.fn(); + + renderHook(() => useDebounceEffect(effect, [], 100)); + + act(() => vi.advanceTimersByTime(99)); + expect(effect).not.toHaveBeenCalled(); + + act(() => vi.advanceTimersByTime(1)); + expect(effect).toHaveBeenCalledOnce(); +}); + +it('Should re-run effect when dependencies change (debounced)', () => { + const effect = vi.fn(); + + const { rerender } = renderHook(({ dep }) => useDebounceEffect(effect, [dep], 100), { + initialProps: { dep: 1 } + }); + + act(() => vi.advanceTimersByTime(100)); + expect(effect).toHaveBeenCalledTimes(1); + + rerender({ dep: 2 }); + + act(() => vi.advanceTimersByTime(99)); + expect(effect).toHaveBeenCalledTimes(1); + + act(() => vi.advanceTimersByTime(1)); + expect(effect).toHaveBeenCalledTimes(2); +}); + +it('Should cancel the previous call if dependency changes before delay', () => { + const effect = vi.fn(); + + const { rerender } = renderHook(({ dep }) => useDebounceEffect(effect, [dep], 100), { + initialProps: { dep: 1 } + }); + + act(() => vi.advanceTimersByTime(50)); + rerender({ dep: 2 }); + + act(() => vi.advanceTimersByTime(100)); + + expect(effect).toHaveBeenCalledTimes(1); +}); + +it('Should support async effect', async () => { + const effect = vi.fn().mockResolvedValue(undefined); + + renderHook(() => useDebounceEffect(effect, [], 100)); + + act(() => vi.advanceTimersByTime(100)); + + expect(effect).toHaveBeenCalledOnce(); +}); diff --git a/packages/core/src/hooks/useDebounceEffect/useDebounceEffect.ts b/packages/core/src/hooks/useDebounceEffect/useDebounceEffect.ts new file mode 100644 index 00000000..688848d1 --- /dev/null +++ b/packages/core/src/hooks/useDebounceEffect/useDebounceEffect.ts @@ -0,0 +1,31 @@ +import { useEffect } from 'react'; + +import { useDebounceCallback } from '../utilities'; + +/** + * @name useDebounceEffect + * @description - Hook that runs an effect function with a debounce delay + * @category Utilities + * + * @template Params The type of the parameters passed to the effect + * @param {() => void | Promise} effect The effect function to execute with debounce + * @param {React.DependencyList} dependency The dependency array for the effect + * @param {number} delay The debounce delay in milliseconds before running the effect + * @returns {void} + * + * @example + * useDebounceEffect(() => { + * console.log("Effect called with delay"); + * }, [value], 500); + */ +export const useDebounceEffect = ( + effect: () => Promise | void, + dependency: React.DependencyList, + delay: number +) => { + const debouncedCallback = useDebounceCallback(effect, delay); + + useEffect(() => { + debouncedCallback(); + }, [debouncedCallback, ...dependency]); +}; diff --git a/packages/core/src/hooks/utilities.ts b/packages/core/src/hooks/utilities.ts index 6f516b7f..040e1130 100644 --- a/packages/core/src/hooks/utilities.ts +++ b/packages/core/src/hooks/utilities.ts @@ -2,6 +2,7 @@ export * from './useConst/useConst'; // timing export * from './useDebounceCallback/useDebounceCallback'; +export * from './useDebounceEffect/useDebounceEffect'; export * from './useDebounceValue/useDebounceValue'; export * from './useEvent/useEvent'; export * from './useLastChanged/useLastChanged';