Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvement: omit useDebounceCallback func arg from dependency array #501

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
60 changes: 38 additions & 22 deletions packages/usehooks-ts/src/useDebounceCallback/useDebounceCallback.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef } from 'react'
import { useMemo, useRef } from 'react'

import debounce from 'lodash.debounce'

Expand Down Expand Up @@ -42,7 +42,7 @@ type ControlFunctions = {
* Ensure proper handling in your code.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type DebouncedState<T extends (...args: any) => ReturnType<T>> = ((
export type DebouncedState<T extends (...args: any[]) => ReturnType<T>> = ((
...args: Parameters<T>
) => ReturnType<T> | undefined) &
ControlFunctions
Expand Down Expand Up @@ -70,24 +70,41 @@ export type DebouncedState<T extends (...args: any) => ReturnType<T>> = ((
* debouncedCallback('react hooks'); // Will invoke the callback after 500 milliseconds of inactivity.
* ```
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useDebounceCallback<T extends (...args: any) => ReturnType<T>>(
func: T,
delay = 500,
options?: DebounceOptions,
): DebouncedState<T> {
const debouncedFunc = useRef<ReturnType<typeof debounce>>()
export function useDebounceCallback<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends (...args: any[]) => ReturnType<T>,
>(func: T, delay = 500, options?: DebounceOptions): DebouncedState<T> {
const pending = useRef<boolean>(false)

useUnmount(() => {
if (debouncedFunc.current) {
debouncedFunc.current.cancel()
}
})
const funcRef = useRef<T>(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<T> = (...args: Parameters<T>) => {
const debouncedFuncInstance = debounce(
(...args: unknown[]) => {
try {
return funcRef.current(...args)
} finally {
pending.current = false
}
},
delay,
debounceOptions,
)

const wrappedFunc: DebouncedState<T> = (...args: unknown[]) => {
pending.current = true
return debouncedFuncInstance(...args)
}

Expand All @@ -96,20 +113,19 @@ export function useDebounceCallback<T extends (...args: any) => ReturnType<T>>(
}

wrappedFunc.isPending = () => {
return !!debouncedFunc.current
return pending.current
}

wrappedFunc.flush = () => {
return debouncedFuncInstance.flush()
}

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
}