Skip to content

Instantly share code, notes, and snippets.

@MatthewCallis
Last active August 23, 2023 17:33
Show Gist options
  • Save MatthewCallis/fac4dd75717d6f0a3de25f89dd90fac7 to your computer and use it in GitHub Desktop.
Save MatthewCallis/fac4dd75717d6f0a3de25f89dd90fac7 to your computer and use it in GitHub Desktop.
Debounce & useDebounce React Hook
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('🧮')
})
})
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