Last active
August 23, 2023 17:33
-
-
Save MatthewCallis/fac4dd75717d6f0a3de25f89dd90fac7 to your computer and use it in GitHub Desktop.
Debounce & useDebounce React Hook
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
import { renderHook } from '@testing-library/react-hooks/dom' | |
import useDebounce, * as allDebounce from 'hooks/useDebounce' | |
describe('debounce', () => { | |
it('calls as expected', () => | |
new Promise<void>((done) => { | |
const callingArgument = 'debounce it!' | |
const debounceCallback = (value: string) => { | |
expect(value).toBe(callingArgument) | |
done() | |
} | |
const debounceFunc = allDebounce.debounce(debounceCallback) | |
debounceFunc(callingArgument) | |
})) | |
it('does not call immediately', () => | |
new Promise<void>((done) => { | |
const startTime = new Date() | |
const debounceCallback = () => { | |
const duration = new Date().valueOf() - startTime.valueOf() | |
expect(duration).toBeGreaterThan(200) | |
done() | |
} | |
const debounceFunc = allDebounce.debounce(debounceCallback) | |
debounceFunc() | |
})) | |
it('never calls callback if continuously called', () => | |
new Promise<void>((done) => { | |
let numCalls = 0 | |
const debounceCallback = () => { | |
numCalls++ | |
} | |
const debounceFunc = allDebounce.debounce(debounceCallback) | |
const testInterval = window.setInterval(() => debounceFunc(), 50) | |
const endTest = (toclear: number) => () => { | |
clearInterval(toclear) | |
expect(numCalls).toBe(0) | |
done() | |
} | |
setTimeout(endTest(testInterval), 500) | |
})) | |
it('calls after its wait time', () => | |
new Promise<void>((done) => { | |
expect.assertions(1) | |
const waitTime = 1000 | |
const startTime = new Date().valueOf() | |
const debounceCallback = () => { | |
const callTime = new Date().valueOf() | |
expect(callTime - startTime).toBeGreaterThanOrEqual(waitTime) | |
done() | |
} | |
const debounceFunc = allDebounce.debounce(debounceCallback, waitTime) | |
debounceFunc() | |
})) | |
it('calls once only after debounce ends', () => | |
new Promise<void>((done) => { | |
let numCalls = 0 | |
const debounceCallback = () => { | |
numCalls++ | |
} | |
const debounceFunc = allDebounce.debounce(debounceCallback) | |
const testInterval = window.setInterval(() => debounceFunc(), 50) | |
const endInterval = (toclear: number) => () => { | |
clearInterval(toclear) | |
} | |
const endTest = () => { | |
expect(numCalls).toBe(1) | |
done() | |
} | |
setTimeout(endInterval(testInterval), 200) | |
setTimeout(endTest, 500) | |
})) | |
}) | |
describe('useDebounce', () => { | |
beforeEach(() => { | |
vi.useFakeTimers() | |
}) | |
afterEach(() => { | |
vi.useRealTimers() | |
vi.restoreAllMocks() | |
}) | |
it('should debounce', () => { | |
const callback = vi.fn() | |
renderHook(() => { | |
const func = useDebounce(callback) | |
func('🧮') | |
}) | |
vi.advanceTimersToNextTimer() | |
expect(callback).toHaveBeenCalledTimes(1) | |
expect(callback).toHaveBeenCalledWith('🧮') | |
}) | |
}) |
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
import { useEffect, useMemo, useRef } from 'react' | |
/** | |
* Creates a debounced function that delays invoking the provided function until after `wait` milliseconds have elapsed since the last time the debounced function was invoked. | |
* Typically used to run an expensive or async function after user interaction. | |
* | |
* @template T The type of the function to debounce. | |
* @param {T} func The function to debounce. | |
* @param {number} [wait=250] The number of milliseconds to delay. | |
* @returns {(...args: Parameters<T>) => void} Returns the new debounced function. | |
* | |
* @example | |
* // Usage with a function that takes one string parameter | |
* const log = (message: string) => console.log(message); | |
* const debouncedLog = debounce(log, 300); | |
* debouncedLog('Hello, world!'); | |
* @see {@link https://gist.github.com/rendall/79a8559ad1b5a022a7923f160f7c429b} | |
*/ | |
export const debounce = <T extends (...args: any[]) => void>(func: T, wait = 250) => { | |
let debounceTimeout: number | null = null | |
return (...args: Parameters<T>): void => { | |
if (debounceTimeout) { | |
window.clearTimeout(debounceTimeout) | |
} | |
debounceTimeout = window.setTimeout(() => { | |
func(...args) | |
}, wait) | |
} | |
} | |
/** | |
* Debounce hook. | |
* @template T The type of the function to debounce. | |
* @param {T} callback The function to debounce. | |
* @param {number} [wait=250] The number of milliseconds to delay. | |
* @returns The debounced function. | |
* @see {@link https://www.developerway.com/posts/debouncing-in-react} | |
* @example | |
* const DebounceWithUseCallbackAndState = () => { | |
* const [value, setValue] = useState("initial"); | |
* const onChange = () => { | |
* console.log("State value:", value); | |
* }; | |
* const debouncedOnChange = useDebounce(onChange); | |
* return ( | |
* <input | |
* onChange={(e) => { | |
* debouncedOnChange(); | |
* setValue(e.target.value); | |
* }} | |
* value={value} | |
* /> | |
* ); | |
* }; | |
*/ | |
const useDebounce = <T extends (...args: any[]) => any>(callback: T, wait = 250): ((...args: any[]) => any) => { | |
const ref = useRef<T>() | |
useEffect(() => { | |
ref.current = callback | |
}, [callback]) | |
const debouncedCallback = useMemo((...args) => { | |
const func = (...args: any[]) => { | |
ref.current?.(...args) | |
} | |
return debounce(func, wait) | |
}, []) | |
return debouncedCallback | |
} | |
export default useDebounce |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment