Last active
March 16, 2020 08:22
-
-
Save astoilkov/a5a6e6c6d11a48fe049ee4d25230653f to your computer and use it in GitHub Desktop.
React hook for debouncing callbacks
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 { useCallback, useLayoutEffect, DependencyList } from 'react' | |
/** | |
* This version bounces each version of the callback. This ensures that the callback | |
* will be called with each state of the application. That's why `deps` is a required argument. | |
* | |
* Previous version used `useRef` for `timeoutId` and didn't have `deps` argument. | |
* This resulted in missing calling the callback for the previous state and necessarily | |
* calling it for the new state. | |
* | |
* Note: In theory bounced callbacks shouldn't access `Ref` instances inside of the | |
* callback. The reason is that `Ref` instance values can change between the time | |
* the callback is executed and the time the bouncing decides to execute it. | |
* That doesn't apply for state and props because they are bound to the current | |
* function context. | |
*/ | |
export default function useDebounceCallback<T extends (...args: any[]) => void>( | |
callback: T, | |
delay: number, | |
deps: DependencyList, | |
): T { | |
let disposed = false | |
let timeoutId: number | undefined | |
let callbackWrapper: Function | undefined | |
/** | |
* Ensures the callback is called immediately after `deps` change | |
* instead of waiting for the `setTimeout` to fire. | |
* | |
* Using `useLayoutEffect` instead of `useEffect` because otherwise the | |
* callback will be called after all `useLayoutEffect` are executed. | |
* `useLayoutEffect` hooks can change `Ref` values and thus change the | |
* `Ref` values accessed in the bounced callback. | |
*/ | |
useLayoutEffect(() => { | |
return () => { | |
/** | |
* Disabling `react-hooks/exhaustive-deps` because we intentionally want to not use refs for | |
* the `disposed` property: | |
* Assignments to the 'disposed' variable from inside React Hook useLayoutEffect will be lost | |
* after each render. To preserve the value over time, store it in a useRef Hook and keep the | |
* mutable value in the '.current' property. Otherwise, you can move this variable directly | |
* inside useLayoutEffect. | |
*/ | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
disposed = true | |
if (callbackWrapper === undefined || timeoutId === undefined) { | |
return | |
} | |
callbackWrapper() | |
clearTimeout(timeoutId) | |
} | |
}, deps) | |
return useCallback<T>( | |
function useDebounceCallback(...args) { | |
if (disposed) { | |
throw new Error( | |
[ | |
'Trying to call an already disposed callback.', | |
'In theory you should never call a disposed callback.', | |
'This is probably a bug.', | |
].join(' '), | |
) | |
} | |
clearTimeout(timeoutId) | |
callbackWrapper = () => { | |
timeoutId = undefined | |
return callback(...args) | |
} | |
timeoutId = window.setTimeout(callbackWrapper, delay) | |
} as T, | |
deps, | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment