From 4edcd559af89cba1bb6d6d47e7f8f5336d37059b Mon Sep 17 00:00:00 2001 From: Nizom Sidiq Date: Wed, 21 Feb 2024 22:59:24 +0700 Subject: [PATCH] omit useDebounceCallback func from dependency array --- .../useDebounceCallback.test.ts | 23 +++++++++ .../useDebounceCallback.ts | 48 +++++++++++++------ 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/packages/usehooks-ts/src/useDebounceCallback/useDebounceCallback.test.ts b/packages/usehooks-ts/src/useDebounceCallback/useDebounceCallback.test.ts index b2f9ee97..ac2fe25d 100644 --- a/packages/usehooks-ts/src/useDebounceCallback/useDebounceCallback.test.ts +++ b/packages/usehooks-ts/src/useDebounceCallback/useDebounceCallback.test.ts @@ -107,4 +107,27 @@ describe('useDebounceCallback()', () => { // The callback should be invoked immediately after flushing expect(debouncedCallback).toHaveBeenCalled() }) + + it('should have pending state', () => { + const delay = 500 + const debouncedCallback = vitest.fn() + const { result } = renderHook(() => + useDebounceCallback(debouncedCallback, delay), + ) + + act(() => { + result.current('argument') + }) + + // The callback must be pending before invoked + expect(debouncedCallback).not.toHaveBeenCalled() + expect(result.current.isPending()).toBe(true) + + // Fast forward time + vitest.advanceTimersByTime(500) + + // The callback must be not pending after invoked + expect(debouncedCallback).toHaveBeenCalled() + expect(result.current.isPending()).toBe(false) + }) }) diff --git a/packages/usehooks-ts/src/useDebounceCallback/useDebounceCallback.ts b/packages/usehooks-ts/src/useDebounceCallback/useDebounceCallback.ts index 2ccf8f04..c703cb3d 100644 --- a/packages/usehooks-ts/src/useDebounceCallback/useDebounceCallback.ts +++ b/packages/usehooks-ts/src/useDebounceCallback/useDebounceCallback.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef } from 'react' +import { useMemo, useRef } from 'react' import debounce from 'lodash.debounce' @@ -79,18 +79,37 @@ export function useDebounceCallback ReturnType>( delay = 500, options?: DebounceOptions, ): DebouncedState { - const debouncedFunc = useRef>() + const pending = useRef(false) - useUnmount(() => { - if (debouncedFunc.current) { - debouncedFunc.current.cancel() - } - }) + const funcRef = useRef(func) + funcRef.current = func const debounced = useMemo(() => { - const debouncedFuncInstance = debounce(func, delay, options) + const debounceOptions = + options?.leading !== undefined || + options?.trailing !== undefined || + options?.maxWait !== undefined + ? { + leading: options?.leading, + trailing: options?.trailing, + maxWait: options?.maxWait, + } + : undefined - const wrappedFunc: DebouncedState = (...args: Parameters) => { + const debouncedFuncInstance = debounce( + (...args: unknown[]) => { + try { + return funcRef.current(...args) + } finally { + pending.current = false + } + }, + delay, + debounceOptions, + ) + + const wrappedFunc: DebouncedState = (...args) => { + pending.current = true return debouncedFuncInstance(...args) } @@ -99,7 +118,7 @@ export function useDebounceCallback ReturnType>( } wrappedFunc.isPending = () => { - return !!debouncedFunc.current + return pending.current } wrappedFunc.flush = () => { @@ -107,12 +126,11 @@ export function useDebounceCallback ReturnType>( } return wrappedFunc - }, [func, delay, options]) + }, [delay, options?.leading, options?.trailing, options?.maxWait]) - // Update the debounced function ref whenever func, wait, or options change - useEffect(() => { - debouncedFunc.current = debounce(func, delay, options) - }, [func, delay, options]) + useUnmount(() => { + debounced.cancel() + }) return debounced }