diff --git a/packages/@mantine/hooks/src/use-debounced-callback/use-debounced-callback.test.ts b/packages/@mantine/hooks/src/use-debounced-callback/use-debounced-callback.test.ts index e177e9bb602..b596ce25e5e 100644 --- a/packages/@mantine/hooks/src/use-debounced-callback/use-debounced-callback.test.ts +++ b/packages/@mantine/hooks/src/use-debounced-callback/use-debounced-callback.test.ts @@ -26,4 +26,59 @@ describe('@mantine/hooks/use-debounced-callback', () => { jest.advanceTimersByTime(100); expect(callback).toHaveBeenCalledWith(3); }); + + it('can be flushed immediately', () => { + const callback = jest.fn(); + const { result } = renderHook(() => useDebouncedCallback(callback, 100)); + result.current(1); + result.current(2); + result.current(3); + result.current.flush(); + expect(callback).toHaveBeenCalledWith(3); + }); + + it('can flush on unmount', () => { + const callback = jest.fn(); + const { result, unmount } = renderHook(() => + useDebouncedCallback(callback, { delay: 100, flushOnUnmount: true }) + ); + result.current(1); + result.current(2); + result.current(3); + unmount(); + expect(callback).toHaveBeenCalledWith(3); + }); + + it('does not call after unmount if timer lapsed', () => { + const callback = jest.fn(); + const { result, unmount } = renderHook(() => + useDebouncedCallback(callback, { delay: 100, flushOnUnmount: false }) + ); + result.current(1); + unmount(); + jest.advanceTimersByTime(100); + expect(callback).not.toHaveBeenCalled(); + }); + + it('does not call on unmount if never called', () => { + const callback = jest.fn(); + const { unmount } = renderHook(() => + useDebouncedCallback(callback, { delay: 100, flushOnUnmount: true }) + ); + unmount(); + expect(callback).not.toHaveBeenCalled(); + }); + + it('does not call on unmount if already called and not called since', () => { + const callback = jest.fn(); + const { result, unmount } = renderHook(() => + useDebouncedCallback(callback, { delay: 100, flushOnUnmount: true }) + ); + result.current(1); + jest.advanceTimersByTime(100); + expect(callback).toHaveBeenCalled(); + callback.mockClear(); + unmount(); + expect(callback).not.toHaveBeenCalled(); + }); }); diff --git a/packages/@mantine/hooks/src/use-debounced-callback/use-debounced-callback.ts b/packages/@mantine/hooks/src/use-debounced-callback/use-debounced-callback.ts index a0089afc6ee..95fe9c90858 100644 --- a/packages/@mantine/hooks/src/use-debounced-callback/use-debounced-callback.ts +++ b/packages/@mantine/hooks/src/use-debounced-callback/use-debounced-callback.ts @@ -1,19 +1,44 @@ import { useCallback, useEffect, useRef } from 'react'; import { useCallbackRef } from '../use-callback-ref/use-callback-ref'; +const noop = () => {}; + export function useDebouncedCallback any>( callback: T, - delay: number + options: number | { delay: number; flushOnUnmount?: boolean } ) { + const delay = typeof options === 'number' ? options : options.delay; + const flushOnUnmount = typeof options === 'number' ? false : options.flushOnUnmount; const handleCallback = useCallbackRef(callback); const debounceTimerRef = useRef(0); - useEffect(() => () => window.clearTimeout(debounceTimerRef.current), []); - return useCallback( - (...args: Parameters) => { + const lastCallback = Object.assign( + useCallback( + (...args: Parameters) => { + window.clearTimeout(debounceTimerRef.current); + const flush = () => { + if (debounceTimerRef.current !== 0) { + debounceTimerRef.current = 0; + handleCallback(...args); + } + }; + lastCallback.flush = flush; + debounceTimerRef.current = window.setTimeout(flush, delay); + }, + [handleCallback, delay] + ), + { flush: noop } + ); + + useEffect( + () => () => { window.clearTimeout(debounceTimerRef.current); - debounceTimerRef.current = window.setTimeout(() => handleCallback(...args), delay); + if (flushOnUnmount) { + lastCallback.flush(); + } }, - [handleCallback, delay] + [lastCallback, flushOnUnmount] ); + + return lastCallback; }