diff --git a/docs/useDebounce.md b/docs/useDebounce.md index a7ef2206..151e9a2a 100644 --- a/docs/useDebounce.md +++ b/docs/useDebounce.md @@ -48,12 +48,12 @@ const Demo = () => { const [ isReady: () => boolean | null, cancel: () => void, -] = useDebounce(fn: Function, ms: number, args: DependencyList = []); +] = useDebounce(fn: Function, ms: number, deps: DependencyList = []); ``` - **`fn`**_`: Function`_ - function that will be called; - **`ms`**_`: number`_ - delay in milliseconds; -- **`args`**_`: DependencyList`_ - array of values that the debounce depends on, in the same manner as useEffect; +- **`deps`**_`: DependencyList`_ - array of values that the debounce depends on, in the same manner as useEffect; - **`isReady`**_`: ()=>boolean|null`_ - function returning current debounce state: - `false` - pending - `true` - called diff --git a/src/__tests__/useDebounce.test.ts b/src/__tests__/useDebounce.test.ts new file mode 100644 index 00000000..128f68a9 --- /dev/null +++ b/src/__tests__/useDebounce.test.ts @@ -0,0 +1,116 @@ +import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks'; +import { DependencyList } from 'react'; +import { useDebounce } from '../index'; +import { UseDebounceReturn } from '../useDebounce'; + +describe('useDebounce', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should be defined', () => { + expect(useDebounce).toBeDefined(); + }); + + it('should return three functions', () => { + const hook = renderHook(() => useDebounce(() => {}, 5)); + + expect(hook.result.current.length).toBe(2); + expect(typeof hook.result.current[0]).toBe('function'); + expect(typeof hook.result.current[1]).toBe('function'); + }); + + function getHook( + ms: number = 5, + dep: DependencyList = [] + ): [jest.Mock, RenderHookResult<{ delay: number; deps: DependencyList }, UseDebounceReturn>] { + const spy = jest.fn(); + return [ + spy, + renderHook(({ delay = 5, deps = [] }) => useDebounce(spy, delay, deps), { + initialProps: { + delay: ms, + deps: dep, + }, + }), + ]; + } + + it('should call passed function after given amount of time', () => { + const [spy] = getHook(); + + expect(spy).not.toHaveBeenCalled(); + jest.advanceTimersByTime(5); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should cancel function call on unmount', () => { + const [spy, hook] = getHook(); + + expect(spy).not.toHaveBeenCalled(); + hook.unmount(); + jest.advanceTimersByTime(5); + expect(spy).not.toHaveBeenCalled(); + }); + + it('first function should return actual state of debounce', () => { + let [, hook] = getHook(); + let [isReady] = hook.result.current; + + expect(isReady()).toBe(false); + hook.unmount(); + expect(isReady()).toBe(null); + + [, hook] = getHook(); + [isReady] = hook.result.current; + jest.advanceTimersByTime(5); + expect(isReady()).toBe(true); + }); + + it('second function should cancel debounce', () => { + const [spy, hook] = getHook(); + const [isReady, cancel] = hook.result.current; + + expect(spy).not.toHaveBeenCalled(); + expect(isReady()).toBe(false); + + act(() => { + cancel(); + }); + jest.advanceTimersByTime(5); + + expect(spy).not.toHaveBeenCalled(); + expect(isReady()).toBe(null); + }); + + it('should reset timeout on delay change', () => { + const [spy, hook] = getHook(50); + + expect(spy).not.toHaveBeenCalled(); + hook.rerender({ delay: 5, deps: [] }); + + jest.advanceTimersByTime(5); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should reset timeout on deps change', () => { + const [spy, hook] = getHook(50, [5, 6]); + + jest.advanceTimersByTime(45); + expect(spy).not.toHaveBeenCalled(); + hook.rerender({ delay: 50, deps: [6, 6] }); + + jest.advanceTimersByTime(45); + expect(spy).not.toHaveBeenCalled(); + jest.advanceTimersByTime(5); + expect(spy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/useDebounce.ts b/src/useDebounce.ts index 59af8776..c9fc7733 100644 --- a/src/useDebounce.ts +++ b/src/useDebounce.ts @@ -1,9 +1,16 @@ -import { DependencyList, useCallback } from 'react'; +import { DependencyList, useEffect } from 'react'; import useTimeoutFn from './useTimeoutFn'; -export default function useDebounce(fn: (...args: any[]) => any, ms: number = 0, args: DependencyList = []) { - const cb = useCallback(fn, args); - const [isReady, cancel] = useTimeoutFn(cb, ms); +export type UseDebounceReturn = [() => boolean | null, () => void]; + +export default function useDebounce( + fn: (...args: any[]) => any, + ms: number = 0, + deps: DependencyList = [] +): UseDebounceReturn { + const [isReady, cancel, reset] = useTimeoutFn(fn, ms); + + useEffect(reset, deps); return [isReady, cancel]; }