A debounce hook for react

Last update: May 15, 2022

Features

Install

yarn add use-debounce
# or
npm i use-debounce --save

Demos

The simplest way to start playing around with use-debounce is with this CodeSandbox snippet: https://codesandbox.io/s/kx75xzyrq7

More complex example with searching for matching countries using debounced input: https://codesandbox.io/s/rr40wnropq (thanks to https://twitter.com/ZephDavies)

Changelog

https://github.com/xnimorz/use-debounce/blob/master/CHANGELOG.md

Simple values debouncing

According to https://twitter.com/dan_abramov/status/1060729512227467264

import React, { useState } from 'react';
import { useDebounce } from 'use-debounce';

export default function Input() {
  const [text, setText] = useState('Hello');
  const [value] = useDebounce(text, 1000);

  return (
    <div>
      <input
        defaultValue={'Hello'}
        onChange={(e) => {
          setText(e.target.value);
        }}
      />
      <p>Actual value: {text}</p>
      <p>Debounce value: {value}</p>
    </div>
  );
}

This hook compares prev and next value using shallow equal. It means, setting an object {} will trigger debounce timer. If you have to compare objects (https://github.com/xnimorz/use-debounce/issues/27#issuecomment-496828063), you can use useDebouncedCallback, that is explained below:

Debounced callbacks

Besides useDebounce for values you can debounce callbacks, that is the more commonly understood kind of debouncing. Example with Input (and react callbacks): https://codesandbox.io/s/x0jvqrwyq

import { useDebouncedCallback } from 'use-debounce';

function Input({ defaultValue }) {
  const [value, setValue] = useState(defaultValue);
  // Debounce callback
  const debounced = useDebouncedCallback(
    // function
    (value) => {
      setValue(value);
    },
    // delay in ms
    1000
  );

  // you should use `e => debounced.callback(e.target.value)` as react works with synthetic events
  return (
    <div>
      <input defaultValue={defaultValue} onChange={(e) => debounced.callback(e.target.value)} />
      <p>Debounced value: {value}</p>
    </div>
  );
}

Example with Scroll (and native event listeners): https://codesandbox.io/s/32yqlyo815

function ScrolledComponent() {
  // just a counter to show, that there are no any unnessesary updates
  const updatedCount = useRef(0);
  updatedCount.current++;

  const [position, setPosition] = useState(window.pageYOffset);

  // Debounce callback
  const debounced = useDebouncedCallback(
    // function
    () => {
      setPosition(window.pageYOffset);
    },
    // delay in ms
    800
  );

  useEffect(() => {
    const unsubscribe = subscribe(window, 'scroll', debounced.callback);
    return () => {
      unsubscribe();
    };
  }, []);

  return (
    <div style={{ height: 10000 }}>
      <div style={{ position: 'fixed', top: 0, left: 0 }}>
        <p>Debounced top position: {position}</p>
        <p>Component rerendered {updatedCount.current} times</p>
      </div>
    </div>
  );
}

Returned value from debounced.callback

Subsequent calls to the debounced function debounced.callback return the result of the last func invocation. Note, that if there are no previous invocations it's mean you will get undefined. You should check it in your code properly.

Example:

it('Subsequent calls to the debounced function `debounced.callback` return the result of the last func invocation.', () => {
  const callback = jest.fn(() => 42);

  let callbackCache;
  function Component() {
    const debounced = useDebouncedCallback(callback, 1000);
    callbackCache = debounced.callback;
    return null;
  }
  Enzyme.mount(<Component />);

  const result = callbackCache();
  expect(callback.mock.calls.length).toBe(0);
  expect(result).toBeUndefined();

  act(() => {
    jest.runAllTimers();
  });
  expect(callback.mock.calls.length).toBe(1);
  const subsequentResult = callbackCache();

  expect(callback.mock.calls.length).toBe(1);
  expect(subsequentResult).toBe(42);
});

Advanced usage

Cancel, maxWait and memoization

  1. Both useDebounce and useDebouncedCallback works with maxWait option. This params describes the maximum time func is allowed to be delayed before it's invoked.
  2. You can cancel debounce cycle, by calling cancel callback

The full example you can see here https://codesandbox.io/s/4wvmp1xlw4

import React, { useState, useCallback } from 'react';
import ReactDOM from 'react-dom';
import { useDebouncedCallback } from 'use-debounce';

function Input({ defaultValue }) {
  const [value, setValue] = useState(defaultValue);
  const debounced = useDebouncedCallback(
    (value) => {
      setValue(value);
    },
    500,
    // The maximum time func is allowed to be delayed before it's invoked:
    { maxWait: 2000 }
  );

  // you should use `e => debounced.callback(e.target.value)` as react works with synthetic events
  return (
    <div>
      <input defaultValue={defaultValue} onChange={(e) => debounced.callback(e.target.value)} />
      <p>Debounced value: {value}</p>
      <button onClick={debounced.cancel}>Cancel Debounce cycle</button>
    </div>
  );
}

const rootElement = document.getElementById('root');
ReactDOM.render(<Input defaultValue="Hello world" />, rootElement);

Flush method

useDebouncedCallback has flush method. It allows to call the callback manually if it hasn't fired yet. This method is handy to use when the user takes an action that would cause the component to unmount, but you need to execute the callback.

import React, { useState, useCallback } from 'react';
import { useDebouncedCallback } from 'use-debounce';

function InputWhichFetchesSomeData({ defaultValue, asyncFetchData }) {
  const debounced = useDebouncedCallback(
    (value) => {
      asyncFetchData;
    },
    500,
    { maxWait: 2000 }
  );

  // When the component goes to be unmounted, we will fetch data if the input has changed.
  useEffect(
    () => () => {
      debounced.flush();
    },
    [debounced]
  );

  return <input defaultValue={defaultValue} onChange={(e) => debounced.callback(e.target.value)} />;
}

Pending method

pending method shows whether component has pending callbacks. Works for both useDebounce and useDebouncedCallback:

function Component({ text }) {
  const debounced = useDebouncedCallback(useCallback(() => {}, []), 500);

  expect(debounced.pending()).toBeFalsy();
  debounced.callback();
  expect(debounced.pending()).toBeTruthy();
  debounced.flush();
  expect(debounced.pending()).toBeFalsy();

  return <span>{text}</span>;
}

leading/trailing calls

Both useDebounce and useDebouncedCallback work with the leading and trailing options. leading param will execute the function once immediately when called. Subsequent calls will be debounced until the timeout expires. trailing option controls whenever to call the callback after timeout again.

For more information on how leading debounce calls work see: https://lodash.com/docs/#debounce

import React, { useState } from 'react';
import { useDebounce } from 'use-debounce';

export default function Input() {
  const [text, setText] = useState('Hello');
  const [value] = useDebounce(text, 1000, { leading: true });

  // value is updated immediately when text changes the first time,
  // but all subsequent changes are debounced.
  return (
    <div>
      <input
        defaultValue={'Hello'}
        onChange={(e) => {
          setText(e.target.value);
        }}
      />
      <p>Actual value: {text}</p>
      <p>Debounce value: {value}</p>
    </div>
  );
}

Options:

You can provide additional options as a third argument to both useDebounce and useDebouncedCallback:

option default Description Example
maxWait - Describes the maximum time func is allowed to be delayed before it's invoked https://github.com/xnimorz/use-debounce#cancel-maxwait-and-memoization
leading - This param will execute the function once immediately when called. Subsequent calls will be debounced until the timeout expires. https://github.com/xnimorz/use-debounce#leading-calls
trailing true This param executes the function after timeout. https://github.com/xnimorz/use-debounce#leading-calls
equalityFn (prev, next) => prev === next Comparator function which shows if timeout should be started

useThrottledCallback

You are able to use throttled callback with this library also (starting 5.2.0 version). For this purpose use:

import useThrottledCallback from 'use-debounce/useThrottledCallback';

or

import { useThrottledCallback } from 'use-debounce';

Several examples:

  1. Avoid excessively updating the position while scrolling.

    const scrollHandler = useThrottledCallback(updatePosition, 100);
    window.addEventListener('scroll', scrollHandler);
  2. Invoke renewToken when the click event is fired, but not more than once every 5 minutes.

    const throttled = useThrottledCallback(renewToken, 300000, { 'trailing': false })
    <button onClick={throttled}>click</button>

All the params for useThrottledCallback are the same as for useDebouncedCallback except maxWait option. As it's not needed for throttle callbacks.

Special thanks:

@tryggvigy — for managing lots of new features of the library like trailing and leading params, throttle callback, etc;

@omgovich — for reducing bundle size.

GitHub

https://github.com/xnimorz/use-debounce
Comments
  • 1. Optimization: Escape from useCallback hell

    Discussion/Proposal

    The specs are failing now because they are created for the current realization. But actually, I'm not sure that the current realization is the best we can do.

    Since your library returns callbacks only it doesn't make sense to update/lose a link to debouncedState at all.

    So I created a version where debouncedState is always memorized. It also makes code way simpler and saves us 57 bytes.

    Thought?

    Reviewed by omgovich at 2020-11-01 11:21
  • 2. Bad example for useDebouncedCallback in README

    in the readme example of useDebouncedCallback you have:

     const debounced = useDebouncedCallback(
        // function
        (value) => {
          setValue(value);
        },
        // delay in ms
        1000
      );
    

    But it's returning an array, so it really should be

    const [debounced] = useDebouncedCallback(

    Reviewed by TonisPiip at 2021-04-07 12:06
  • 3. Infinite startTimer loop causing high CPU load

    Hello!

    I'm not able to give exact reproduction steps, but this screenshot demonstrates the problem: image

    I'm using useDebouncedCallback in a regular component. The component itself is not re-rendered, and there is nothing that could call the callback itself.

    Version of React: 16.9.46

    Thanks for help!

    Reviewed by gtanczyk at 2021-02-14 10:17
  • 4. Slight confusion between v4 `callPending` (function) and new `pending` (boolean)

    Hi there, first of all, thanks for providing support and putting so much effort into this hook. I'm using it in production code and it works like a charm, love the simplicity!

    While the adoption for v5 is not as widespread, what do you think of renaming debounced.pending() to debounced.isPending()?

    I was dealing with a bug where I was calling debounce.pending() instead of flush() as I was used to the old v4 names, and I had to read the documentation twice to realize to realize my mistake (it was, in the end, my mistake, fully aware of that).

    I feel that some packages adopting the isBoolean() approach makes it more clear for developers. Of course, it is just a suggestion.

    Again, thank you for your work!

    Yuan

    Reviewed by yuanworks at 2020-10-04 02:45
  • 5. How to use debounce for EventListeners?

    Example:

    const windowSizes = useDebounce(window.pageYOffset, 1000)
      const handleScroll = event => {
        //how to debounse windowSizes change?
      }
    
    
      useEffect(() => {
        window.addEventListener('scroll', handleScroll)
    
        return () => {
          window.removeEventListener('scroll', handleScroll)
        }
      }, [])
    
    Reviewed by ZiiMakc at 2019-01-06 12:18
  • 6. Would be great to configure trailing

    Lodash's implementation of debounce has a trailing option which is necessary to specify in some cases: https://lodash.com/docs/4.17.15#debounce

    It would be nice to have that option for this library.

    Reviewed by scottmas at 2019-09-02 23:20
  • 7. useDebounce is broken

    I'll try to explain what's going on. I copy pasted the source code and marked some lines with comments so that I can refer to them while explaining.

          functionTimeoutHandler.current = setTimeout(() => {
            if (!isComponentUnmounted.current) {
              // MARKER: X
              debouncedFunction.apply(null, arguments);
            }
            // MARKER: Z
            cancelDebouncedCallback();
          }, delay);
    
      React.useEffect(() => {
        if (debouncedState[0] !== value) {
          // MARKER: Y
          debouncedCallback[0](value);
        }
      });
    

    Steps 2-6 happen in the same event loop.

    1. Value updates. Timeout is scheduled.
    2. X is triggered. It rerenders the component synchronously.
    3. Y is triggered, but for the closure of previous render. debouncedState[0] and value have different values. Y reschedules a timeout. Remember that we are still in the call stack of "Step 1".
    4. Funny enough, Y is triggered again. This time, it is for the closure of current render. debouncedState[0] and value have different values. Y reschedules a timeout. This step is actually not needed for a bug to occur. Step 3 is enough.
    5. Call stack rewinds up and we are back in "Step 1". The next line is Z.
    6. Z is triggered, cancelling all previously scheduled timeouts in steps 3 and 4.

    Possible solution:

    First, I suggest using eslint-plugin-react-hooks and sticking to its rules. Otherwise it's too easy to make mistakes. Second, I think trying to save a few hundred bytes is unnecessary. It takes a lot away from code readability.

    export default function useDebounce(value, delay, options = {}) {
      const [state, dispatch] = React.useState(value);
      const [callback, cancel] = useDebouncedCallback(useCallback((value) => dispatch(value), []), delay, options);
    
      React.useEffect(() => {
        callback(value);
      }, [callback, value]);
    
      return [state, cancel];
    }
    
    
    Reviewed by anilanar at 2019-05-16 11:12
  • 8. Jest 28.0.0 and jest-environment-jsdom fails to import use-debounce in tests

    Describe the bug Importing the package does not work since the Jest 28.0.0 update in combination with jest-environment-jsdom.

    To Reproduce

    1. update jest to 28.0.0
    2. use jest-environment-jsdom (28.0.0)
    3. create a test that uses use-debounce
    4. run the test

    Expected behavior The test should run successfully.

    use-debounce version: 8.0.0

    Additional Information This problem is also in other packages (described in their docs) https://jestjs.io/docs/upgrading-to-jest28#packagejson-exports Here is the analysis of the problem and their workaround, which works: https://github.com/microsoft/accessibility-insights-web/pull/5421#issuecomment-1109168149 https://github.com/microsoft/accessibility-insights-web/pull/5421/commits/9ad4e618019298d82732d49d00aafb846fb6bac7

    Reviewed by Glup3 at 2022-05-03 09:15
  • 9. 5.0.3 — useDebouncedCallback export issue

    Hey, firstly thank you for your work on the package, appreciate it.

    There is an issue in the latest version 5.0.3, it seems like useDebouncedCallback hasn't been exported properly

    ModuleNotFoundError: Module not found: Error: Package path ./lib/useDebouncedCallback is not exported from package /node_modules/use-debounce (see exports field in /node_modules/use-debounce/package.json)
    
    Reviewed by kamranahmedse at 2020-11-09 12:25
  • 10. How to restore the behavior of v3 in v4?

    How do I preserve the old behavior from version 3? The PR #62 introduced new options, but I tried various combinations of the flags and none gave the old effect. Is this even possible? Maybe the current behavior relies on bugs that I should fix? Am I stuck with v3 now?

    Context: Just spent 2 hours to pinpoint my new form validation errors to the upgrade of use-debounce to version 4.0.0. The funny consequence is that depending on new options' values of use-debounce, yup (form validation library) either throws errors for all form fields in the form, where messages are just valid field values (probably timing-related), or does not seem to be running at all.

    Repro: I don't have a minimal example, but this is the snippet that breaks in my app: https://github.com/neherlab/covid19_scenarios/blob/d12969d1cef3c07abb3d54b914c38001aec851e2/src/components/Main/Main.tsx#L119-L153 Reproducible by upgrading "use-debounce" to "4.0.0" and then changing any form value.

    cc: @tryggvigy

    Thanks for your help!

    Reviewed by ivan-aksamentov at 2020-09-07 23:57
  • 11. Complete support for leading, trailing, maxWait

    This PR

    • Addresses https://github.com/xnimorz/use-debounce/issues/60
    • Changes useDebouncedCallback to mirror lodash's implementation of debounce.
    • Support using rAF when omitting wait
    • Ability to implement throttling without trailing edge call
      • wait: 200, {leading: true, trailing: false, maxWait: 200}

    This is a lot of changes to the source code but it was easier to start with lodash code and work backwards from there. Let me know what you think.

    Things to note

    • The requestAnimationFrame is accessed via window.requestAnimationFrame. I'm not an experienced library author so I'm not sure if this is safe in all contexts (server side rendering, etc.). Looks like lodash uses https://github.com/lodash/lodash/blob/master/.internal/root.js to choose the right global object. I guess this is infamously painful in the JS ecosystem :)
    Reviewed by tryggvigy at 2020-08-31 22:02
  • 12. "leading: true" not working for v7

    Describe the bug For the versions 7.0.0 and 7.0.1, setting { leading: true } does not trigger an immediate change in useDebounce. It works in 6.0.1 and below.

    To Reproduce

    • The sandbox example linked to in the readme file (https://codesandbox.io/s/rr40wnropq) uses [email protected]. Add the option { leading: true } and observe that the debounced value is updated immedately for the first change.
    • Change the version number to 7.0.0 or 7.0.1, and observe that the behavior is now as if { leading: true } is not given.

    Expected behavior Setting { leading: true } should trigger immediately for the first change, and subsequently trigger immediately on changes after the wait timeout has expired.

    use-debounce version: 7.0.0 and 7.0.1

    Reviewed by mntnoe at 2021-12-14 09:34
React Debounce Hook App Example

React Debounce Hook App Example A search bar to find users from GitHub, with implementation of denouncing to prevent API call on every key press. View

Dec 7, 2021
A debounce hook that dynamically adjusts to input speed

react-dynamic-debounce A debounce hook that dynamically adjusts to input speed.

Dec 26, 2021
React debounce hooks based on state changed.

Debounce Hooks React debounce hooks based on state changed. Live Preview / Demo In Here Installation npm i @bakunya/debounce-hooks Basic Usage Note d

Apr 29, 2022
Keep-react-hook-form - A lib to store react-hook-form values in session storage

Keep-react-hook-form - A lib to store react-hook-form values in session storage

Jan 27, 2022
A React hook compatible with React 16.6's Suspense component.

useFetch useFetch is a React hook that supports the React 16.6 Suspense component implementation. The design decisions and development process for thi

Mar 18, 2022
React hook to handle any async operation in React components, and prevent race conditions

React-async-hook This library only does one small thing, and does it well. Don't expect it to grow in size, because it is feature complete: Handle fet

May 14, 2022
useFormless allow you to control forms in React using react-hook approach

useFormless react-useformless is a simple library that allows you to control forms with react-hooks approach Why useFormless? Works with nested forms

Jan 11, 2022
A React hook for the React Native Dimensions API.

useDimensions A React hook for the React Native Dimensions API. Install npm install use-dimensions --save, or yarn add use-dimensions Use Screen and W

Nov 19, 2021
React Hook for pub-sub behavior using React Router.

useReactRouter useReactRouter is a React Hook that provides pub-sub behavior for react-router. Unlike the withRouter Higher-Order Component, useReactR

May 7, 2022
☯️ React hook to determine if you are on the server, browser, or react native
☯️ React hook to determine if you are on the server, browser, or react native

useSSR ☯️ React hook to determine if you are on the server, browser, or react native Need to know when you're on the server, in the browser or in reac

Apr 15, 2022
A react hook which lets you automatically synchronize a value to a server with react-query

useReactQueryAutoSync A helpful react hook for building interfaces which require autosave. Read more about the motivation and design in the original b

May 3, 2022
A small package of custom React hooks that are useful for debugging changes in React hook dependencies across renders

use-debugger-hooks This is a package of custom React hooks that are useful for debugging dependency changes between renders. Most act as drop in repla

May 13, 2022
React-Scrollbar-Size is a React hook designed to calculate the size of the user agent's horizontal and vertical scrollbars.

React-Scrollbar-Size is a React hook designed to calculate the size of the user agent's horizontal and vertical scrollbars. It will also detect when the size of the scrollbars change, such as when the user agent's zoom factor changes.

Mar 3, 2022
React validatable form hook that is used to create dynamic client side validations on react forms

React Validatable Form React validatable form hook that is used to create dynamic client side validations on React forms. Table of Contents Install Ge

Apr 19, 2022
React-use-regex - A react hook for processing regular expressions

React-use-regex - A react hook for processing regular expressions

Jan 27, 2022
Viewport units (vw, vh, vmin, vmax) + useUnits hook for React, React Native and Expo
Viewport units (vw, vh, vmin, vmax) + useUnits hook for React, React Native and Expo

Viewport units (vw, vh, vmin, vmax) + useUnits hook for React, React Native and Expo.

May 2, 2022
⚛️ useWorker() - A React Hook for Blocking-Free Background Tasks
⚛️ useWorker() - A React Hook for Blocking-Free Background Tasks

Use web workers with react hook https://useworker.js.org/ ?? Features Run expensive function without blocking UI (Show live gif) Supports Promises pat

May 11, 2022
React hook for determining the size of a component

@rehooks/component-size Install yarn add @rehooks/component-size Usage import { useRef } from 'react' import useComponentSize from '@rehooks/component

Apr 19, 2022
React hook for updating the document-title

@rehooks/document-title React hook for updating the document-title Note: This is using the new React Hooks API Proposal which is subject to change unt

Feb 4, 2022