Skip to content

Instantly share code, notes, and snippets.

@astoilkov
Last active March 16, 2020 08:22
Show Gist options
  • Select an option

  • Save astoilkov/a5a6e6c6d11a48fe049ee4d25230653f to your computer and use it in GitHub Desktop.

Select an option

Save astoilkov/a5a6e6c6d11a48fe049ee4d25230653f to your computer and use it in GitHub Desktop.

Revisions

  1. astoilkov revised this gist Mar 14, 2020. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions useDebounceCallback.ts
    Original file line number Diff line number Diff line change
    @@ -14,11 +14,11 @@ import { useCallback, useLayoutEffect, DependencyList } from 'react'
    * That doesn't apply for state and props because they are bound to the current
    * function context.
    */
    export default <T extends (...args: any[]) => void>(
    export default function useDebounceCallback<T extends (...args: any[]) => void>(
    callback: T,
    delay: number,
    deps: DependencyList,
    ): T => {
    ): T {
    let disposed = false
    let timeoutId: number | undefined
    let callbackWrapper: Function | undefined
  2. astoilkov revised this gist Mar 14, 2020. No changes.
  3. astoilkov created this gist Mar 14, 2020.
    79 changes: 79 additions & 0 deletions useDebounceCallback.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,79 @@
    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 <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,
    )
    }