-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: improve debounce implementation
- Loading branch information
Showing
6 changed files
with
255 additions
and
13 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import React from 'react'; | ||
|
||
/** | ||
* A custom React hook that debounces a given function and returns several methods for controlling the debounced behavior. | ||
* @param {function} func - The function to be debounced. | ||
* @param {number} wait - The wait time (in milliseconds) before the function is debounced. | ||
* @returns {[function, function, function]} - An array containing four functions: | ||
* - handleDebouncily: A function that will handle the debounced function call. | ||
* - flush: A function that will immediately call the debounced function and clear the debounce timer. | ||
* - cancel: A function that will cancel the debounce timer without calling the debounced function. | ||
*/ | ||
export const useDebounce = (func, wait) => { | ||
const debounceRef = React.useRef({ timer: null, wait: null, func }); | ||
debounceRef.current.wait = wait; | ||
debounceRef.current.func = func; | ||
|
||
const handleDebouncily = React.useCallback( | ||
(...args) => { | ||
if (debounceRef.current.timer) { | ||
clearTimeout(debounceRef.current.timer); | ||
debounceRef.current.timer = null; | ||
} | ||
debounceRef.current.args = args; | ||
debounceRef.current.timer = setTimeout(() => { | ||
debounceRef.current.timer = null; | ||
debounceRef.current.func(...args); | ||
}, debounceRef.current.wait); | ||
}, | ||
[debounceRef] | ||
); | ||
|
||
const flush = React.useCallback(() => { | ||
if (debounceRef.current.timer) { | ||
clearTimeout(debounceRef.current.timer); | ||
debounceRef.current.timer = null; | ||
} | ||
debounceRef.current.func(...debounceRef.current.args); | ||
}, [debounceRef]); | ||
|
||
const cancel = React.useCallback(() => { | ||
if (debounceRef.current.timer) { | ||
clearTimeout(debounceRef.current.timer); | ||
debounceRef.current.timer = null; | ||
} | ||
}, [debounceRef]); | ||
|
||
React.useEffect( | ||
() => () => { | ||
if (debounceRef.current.timer) { | ||
clearTimeout(debounceRef.current.timer); | ||
debounceRef.current.timer = null; | ||
} | ||
}, | ||
[debounceRef] | ||
); | ||
|
||
return [handleDebouncily, flush, cancel]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { renderHook, act } from '@testing-library/react-hooks'; | ||
import { useDebounce } from './useDebounce'; | ||
|
||
jest.useFakeTimers(); | ||
|
||
describe('useDebounce', () => { | ||
const mockFunc = jest.fn(); | ||
const wait = 500; | ||
const args = [1, 2, 3]; | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should return an array containing four functions', () => { | ||
const { result } = renderHook(() => useDebounce(mockFunc, wait)); | ||
|
||
expect(result.current).toHaveLength(3); | ||
expect(result.current[0]).toBeInstanceOf(Function); | ||
expect(result.current[1]).toBeInstanceOf(Function); | ||
expect(result.current[2]).toBeInstanceOf(Function); | ||
}); | ||
|
||
it('should debounce the function call using the provided wait time', () => { | ||
const { result } = renderHook(() => useDebounce(mockFunc, wait)); | ||
const [handleDebouncily] = result.current; | ||
|
||
act(() => { | ||
handleDebouncily(...args); | ||
}); | ||
|
||
// Before debounce interval | ||
expect(mockFunc).not.toHaveBeenCalled(); | ||
|
||
act(() => { | ||
jest.advanceTimersByTime(wait - 1); | ||
}); | ||
|
||
// During debounce interval | ||
expect(mockFunc).not.toHaveBeenCalled(); | ||
|
||
act(() => { | ||
jest.advanceTimersByTime(1); | ||
}); | ||
|
||
// After debounce interval | ||
expect(mockFunc).toHaveBeenCalledTimes(1); | ||
expect(mockFunc).toHaveBeenCalledWith(...args); | ||
}); | ||
|
||
it('should immediately call the function and clear the debounce timer using flush', () => { | ||
const { result } = renderHook(() => useDebounce(mockFunc, wait)); | ||
const [handleDebouncily, flush] = result.current; | ||
|
||
act(() => { | ||
handleDebouncily(...args); | ||
flush(); | ||
}); | ||
|
||
expect(mockFunc).toHaveBeenCalledTimes(1); | ||
expect(mockFunc).toHaveBeenCalledWith(...args); | ||
|
||
act(() => { | ||
jest.runOnlyPendingTimers(); | ||
}); | ||
|
||
expect(mockFunc).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should cancel the debounce timer using cancel', () => { | ||
const { result } = renderHook(() => useDebounce(mockFunc, wait)); | ||
const [handleDebouncily, , cancel] = result.current; | ||
|
||
act(() => { | ||
handleDebouncily(...args); | ||
cancel(); | ||
}); | ||
|
||
act(() => { | ||
jest.runOnlyPendingTimers(); | ||
}); | ||
|
||
expect(mockFunc).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('should clear the debounce timer on unmount', () => { | ||
const { result, unmount } = renderHook(() => useDebounce(mockFunc, wait)); | ||
const [handleDebouncily] = result.current; | ||
|
||
act(() => { | ||
handleDebouncily(...args); | ||
}); | ||
|
||
// Before unmount | ||
expect(mockFunc).not.toHaveBeenCalled(); | ||
|
||
unmount(); | ||
|
||
// After unmount | ||
act(() => { | ||
jest.runOnlyPendingTimers(); | ||
}); | ||
|
||
expect(mockFunc).not.toHaveBeenCalled(); | ||
}); | ||
}); |