GithubHelp home page GithubHelp logo

xnimorz / use-debounce Goto Github PK

View Code? Open in Web Editor NEW
2.7K 9.0 107.0 1.83 MB

A debounce hook for react

License: MIT License

JavaScript 0.43% TypeScript 99.57%
react-hooks react-hook debounce react usedebounce react-use

use-debounce's Introduction

useDebounce, useDebouncedCallback & useThrottledCallback

React libraries for debouncing without tears!

  • Small size < 1 Kb
  • Compatible with underscore / lodash impl — learn once, use everywhere
  • Server-rendering friendly!

Features

Install

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

Copy paste guidance:

use-debounce

Simple usage: https://codesandbox.io/s/kx75xzyrq7

Debounce HTTP request: https://codesandbox.io/s/rr40wnropq

Debounce HTTP request with leading param: https://codesandbox.io/s/cache-example-with-areas-and-leading-param-119r3i

use-debounce callback

Simple usage: https://codesandbox.io/s/x0jvqrwyq

Combining with native event listeners: https://codesandbox.io/s/32yqlyo815

Cancelling, maxWait and memoization: https://codesandbox.io/s/4wvmp1xlw4

HTTP requests: https://codesandbox.io/s/use-debounce-callback-http-y1h3m6

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 (#27 (comment)), 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(e.target.value)` as react works with synthetic events
  return (
    <div>
      <input
        defaultValue={defaultValue}
        onChange={(e) => debounced(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);
    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()

Subsequent calls to the debounced function debounced 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` return the result of the last func invocation.', () => {
  const callback = jest.fn(() => 42);

  let callbackCache;
  function Component() {
    const debounced = useDebouncedCallback(callback, 1000);
    callbackCache = debounced;
    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 } 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(e.target.value)` as react works with synthetic events
  return (
    <div>
      <input
        defaultValue={defaultValue}
        onChange={(e) => debounced(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 } 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(e.target.value)}
    />
  );
}

isPending method

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

import React, { useCallback } from 'react';

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

  expect(debounced.isPending()).toBeFalsy();
  debounced();
  expect(debounced.isPending()).toBeTruthy();
  debounced.flush();
  expect(debounced.isPending()).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 [useDebounce ONLY] 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.

use-debounce's People

Contributors

andarist avatar andreirailean avatar appden avatar astj avatar dependabot[bot] avatar dohomi avatar elektronik2k5 avatar gautiert avatar ischenkodv avatar jfschwarz avatar koenpunt avatar lemol avatar lumberman avatar lytc avatar matewilk avatar neoromantic avatar nmussy avatar omgovich avatar pringels avatar sarunast avatar seruco avatar thibaultboursier avatar tony avatar tryggvigy avatar vkrol avatar wangcch avatar wuzzeb avatar xianjiezh avatar xnimorz avatar yuanworks avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

use-debounce's Issues

Changelog for 5.0.4?

Hi,

5.0.4 was published to npmjs.com five days ago. Are there are release notes/changelogs for it?

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!

Return callPending for useDebounced as well?

Wanted to use the callPending flag, but now see that it's only returned for useDebouncedCallback, not useDebounced. Would it be possible to add callPending to useDebounced as well?

Why not export default?

There is no other export from the module, why not export default? So we can use:

import useDebounce from 'use-debounce';

in place of current

import { useDebounce } from 'use-debounce';

React DOM peer dependency?

Just installed this hook in my React Native project and got an unsatisfied peer dependency warning for react-dom. Looking through the code it doesn't seem like react-dom is used anywhere expect the tests.

Would you mind if I submitted a PR to move react-dom to a dev dependency instead of a peer dependency?

Thanks for creating this! 😄

useDeferredValue

This is not an Issue, but I'm posting FYI.

The useDebounce hook is very similar to the experimental React useDeferredValue hook.

However, the useDeferredValue value hook is buggy, and I'd not recommend using it at this time.  One developer wrote that this hook should be renamed to useDebouncedValue.  I agree this would be a better name.

The useDebounce hook is stable and it has more options.

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

How to set value with immediate feedback

I've this use case:

const SongEditor = () => {
  const edit = useContext(EditContext);
  const { editSong, text, setText, setHasChanges, songFile } = edit;
  const [debouncedText] = useDebounce(text, 800);

  if (!editSong) {
    return null;
  }

  return <div> .... {debouncedText} </div>

editSong is dynamic, so when i clic a given song, it is loaded on the SongEditor, but with this code i've the first 800 delay, which is not needed and ugly: the only need is when the user starts editing the text. I've looked for a way to access the internal hook state to set it without the initial delay, but dispatch is not returned. Any advice?

react synthetic event error

Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property target on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist(). See https://fb.me/react-event-pooling for more information.
I add persist event, but nothing happened
image
image

Callback can't return a promise

const { callback } = useDebouncedCallback(
  async () => {
    return new Promise((r) => setTimeout(r, 3000));
  },
  300,
  { leading: true }
);

React.useEffect(() => {
    callback().then(() => console.log('yay'));
},[]);

This code will fail which is very misleading as the typing for the callback is a Promise<void>

Callback doesn't fire if component unmounts before delay elapses

I'm using useDebouncedCallback and noticed that if the component unmounts (such as an edit window that is closed) before the delay finishes the callback never fires. This makes sense, as you wouldn't want a callback to set state in a component that has already unmounted. But it would be nice if there was some way for me to trigger the callback manually if it hasn't fired yet. Maybe a callPending function in addition to cancel? I'd then be able to call that function when the user takes an action that would cause the component to unmount.

Types aren't recoginzed

My IDE isn't picking up the TypeScript definitions.

Adding the following line to the package.json fixes the issue:

"types": "./lib"

I can make a quick PR if you would like. 👍

Thanks for the great little hook!

Stops working after Next.js Fast Refresh

use-debounce stops working altogether after any changes to code are made in a Next.js project.

You can see a reproduction example here: https://codesandbox.io/s/hardcore-sara-ueu2k

Looks fine on first load, but doesn't call the input callback anymore after refresh.

After initial load

After [Fast Refresh] done

Some people are commenting similar situations over at the Next.js repo, experiencing breaking libs after they decided to rolled out their new "Fast Refresh" implementation. Oftentimes, these ended up being undiscovered bugs in the original libraries rather than a problem with Next.js itself.

Related Next.js issue: vercel/next.js#13268

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)
    }
  }, [])

Interest in exposing useThrottledCallback through this library?

useThrottledCallback can easily be implemented with the newer useDebouncedCallback api:

import {
  useDebouncedCallback,
  Options,
  Debounced,
} from './useDebouncedCallback';

/**
 * Creates a throttled function that only invokes `func` at most once per
 * every `wait` milliseconds (or once per browser frame). The throttled function
 * comes with a `cancel` method to cancel delayed `func` invocations and a
 * `flush` method to immediately invoke them. Provide `options` to indicate
 * whether `func` should be invoked on the leading and/or trailing edge of the
 * `wait` timeout. The `func` is invoked with the last arguments provided to the
 * throttled function. Subsequent calls to the throttled function return the
 * result of the last `func` invocation.
 *
 * **Note:** If `leading` and `trailing` options are `true`, `func` is
 * invoked on the trailing edge of the timeout only if the throttled function
 * is invoked more than once during the `wait` timeout.
 *
 * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
 * until the next tick, similar to `setTimeout` with a timeout of `0`.
 *
 * If `wait` is omitted in an environment with `requestAnimationFrame`, `func`
 * invocation will be deferred until the next frame is drawn (typically about
 * 16ms).
 *
 * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
 * for details over the differences between `throttle` and `debounce`.
 *
 * @category Function
 * @param {Function} func The function to throttle.
 * @param {number} [wait=0]
 *  The number of milliseconds to throttle invocations to; if omitted,
 *  `requestAnimationFrame` is used (if available).
 * @param {Object} [options={}] The options object.
 * @param {boolean} [options.leading=true]
 *  Specify invoking on the leading edge of the timeout.
 * @param {boolean} [options.trailing=true]
 *  Specify invoking on the trailing edge of the timeout.
 * @returns {Function} Returns the new throttled function.
 * @example
 *
 * // Avoid excessively updating the position while scrolling.
 * const scrollHandler = useThrottledCallback(updatePosition, 100)
 * window.addEventListener('scroll', scrollHandler)
 *
 * // 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>
 *
 * // Cancel the trailing throttled invocation.
 * jQuery(window).on('popstate', throttled.cancel)
 */
export function useThrottledCallback<T extends (...args: never[]) => unknown>(
  func: T,
  wait: number,
  { maxWait = wait, leading = true, trailing = true }: Options = {},
): Debounced<T> {
  return useDebouncedCallback(func, wait, {
    maxWait,
    leading,
    trailing,
  });
}

I think it would be nice to also add a similar jsdoc codeblock for useDebouncedCallback so code editors can pick them up and show inline.

/**
 * Creates a debounced function that delays invoking `func` until after `wait`
 * milliseconds have elapsed since the last time the debounced function was
 * invoked, or until the next browser frame is drawn. The debounced function
 * comes with a `cancel` method to cancel delayed `func` invocations and a
 * `flush` method to immediately invoke them. Provide `options` to indicate
 * whether `func` should be invoked on the leading and/or trailing edge of the
 * `wait` timeout. The `func` is invoked with the last arguments provided to the
 * debounced function. Subsequent calls to the debounced function return the
 * result of the last `func` invocation.
 *
 * **Note:** If `leading` and `trailing` options are `true`, `func` is
 * invoked on the trailing edge of the timeout only if the debounced function
 * is invoked more than once during the `wait` timeout.
 *
 * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
 * until the next tick, similar to `setTimeout` with a timeout of `0`.
 *
 * If `wait` is omitted in an environment with `requestAnimationFrame`, `func`
 * invocation will be deferred until the next frame is drawn (typically about
 * 16ms).
 *
 * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
 * for details over the differences between `debounce` and `throttle`.
 *
 * @category Function
 * @param {Function} func The function to debounce.
 * @param {number} [wait=0]
 *  The number of milliseconds to delay; if omitted, `requestAnimationFrame` is
 *  used (if available).
 * @param {Object} [options={}] The options object.
 * @param {boolean} [options.leading=false]
 *  Specify invoking on the leading edge of the timeout.
 * @param {number} [options.maxWait]
 *  The maximum time `func` is allowed to be delayed before it's invoked.
 * @param {boolean} [options.trailing=true]
 *  Specify invoking on the trailing edge of the timeout.
 * @returns {Function} Returns the new debounced function.
 * @example
 *
 * // Avoid costly calculations while the window size is in flux.
 * const resizeHandler = useDebouncedCallback(calculateLayout, 150);
 * window.addEventListener('resize', resizeHandler)
 *
 * // Invoke `sendMail` when clicked, debouncing subsequent calls.
 * const clickHandler = useDebouncedCallback(sendMail, 300, {
 *   leading: true,
 *   trailing: false,
 * })
 * <button onClick={clickHandler}>click me</button>
 *
 * // Ensure `batchLog` is invoked once after 1 second of debounced calls.
 * const debounced = useDebouncedCallback(batchLog, 250, { 'maxWait': 1000 })
 * const source = new EventSource('/stream')
 * source.addEventListener('message', debounced)
 *
 * // Cancel the trailing debounced invocation.
 * window.addEventListener('popstate', debounced.cancel)
 *
 * // Check for pending invocations.
 * const status = debounced.pending() ? "Pending..." : "Ready"
 */
export function useDebouncedCallback(...) { ... }

Misleading memoization in useDebouncedCallback

Heya, thanks for the lib!

I have conceptual disagreement with how it works wrt the returned callback though. My expectation, given the name useCallback and memoization being mentioned in the docs, that it behaves similar to useCallback. Or more specifically returns a new callback when the given callback changes. I saw this issue and agree with the assessment that a deps array wouldn't be the right approach.

Instead I'd propose changing callback from being a ref, to be part of the debounced useCallback deps here:

[maxWait, delay, cancelDebouncedCallback, leading, trailing]

(if that's feasible, you know the code better than me)

Here's some example code showing how it surprised me. There's a commented-out fix on line 17, but I don't think it's super elegant.
https://codesandbox.io/s/quirky-monad-hedgy?file=/src/App.js

Happy to create a PR if you agree with my assessment!

ReactNative warning: Package ignored

Screen Shot 2020-11-10 at 09 38 04

Therefore, the app and hook seem to run ok.

Env:

{
  "react": "16.13.1",
  "react-dom": "16.13.1",
  "react-native": "0.63.3",
  "use-debounce": "5.0.3"
}

NODE_VERSION=15.1.0
OS=MacOS 11.0.1 beta

Value debouncing breaks in react native on expo when component is refreshed

Just thought to let you know, a simple const [valueDebounced] = useDebounce(value, 100) tested in react native on expo works fine - until the component is refreshed (because you've made a code change and saved the file). Unless the change altered the initial value of the value state, the link between the value and the valueDebounced is broken and valueDebounced no longer updates at all.

Is this hook intended for debouncing function invocation?

Hi there! I came across this library trying to find something equivalent to _.debounce but using React hooks. Something like this:

function Input({ onChange }) {
  const handleChange = useDebounce(onChange, 1000);
  return (
    <input
      defaultValue={'Hello'}
      onChange={handleChange}
    />
  );
}

After looking at the implementation, it seems like it's more like a time-based cache. Do I have that right? It might be useful to provide an example of the more commonly understood (I think?) kind of debouncing if it's possible, or a clarification to specify the intention a little more explicitly otherwise.

`callPending` should be wrapped in `useCallback`

The documented example to use callPending when the components unmounts violates the principle of correctly defining the dependencies for useEffect.

Example code:

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

Eslint rule violation:

React Hook useEffect has a missing dependency: 'callPending'. Either include it or remove the dependency array. eslint(react-hooks/exhaustive-deps)

Unfortunately, callPending has a new identity on every render, so listing it as a dependency won't achieve the desired result.

I feel the lib should memoize the returned callPending by using useCallback and then the example in the README should be updated to correctly follow the react-hooks/exhaustive-deps rule.

If you think this would make sense, I'm happy to submit a PR. Let me know :)

How to know if the value is debouncing?

I filter a very large list and use the useDebounce hook to delay the entry of the searchValue:

const [value, setValue] = useState('');

const [debouncedSearchValue] = useDebounce(value, 500);
useEffect(() => {
  setSearchValue(debouncedSearchValue);
}, [debouncedSearchValue]);

I would love to show a filtering... message or something like that during the debounce. Is there a way to know whether useDebounce is still debouncing?

Support lodash style throttling options for trailing+maxWidth

https://github.com/lodash/lodash/blob/master/throttle.js

It would be nice to be able to do this:

useDebouncedCallback(callback, 300, {
  leading: true,
  trailing: false,
  maxWait: 300,
}),

where the trailing edge is turned off.
Let's say the function is called twice in the first 300ms.

What happened
Debounced function is called twice even though trailing is false (this is due to the maxWait logic).

What I would have expected
Debounced function to have been called once.

From Debouncing and Throttling Explained Through Examples which is linked in lodash source:

The new maxWait option (only in Lodash at the moment) is not covered in this article but it can be very useful. Actually, the throttle function is defined using _.debounce with maxWait, as you see in the lodash source code.

Proper way to access outside values within the debounced function

Hello, I've started using hooks recently and I am sorry if this issue is not related to this lib.

Could you tell me please is there a way to access updated state values within the debounced function? Let me clarify the problem via comments on the minimal example:

function App() {
    const [value, setValue] = useInputState('') // this just encapsulates passing of the e => e.target.value

    console.log({valueInRender: value})
    const [debounced] = useDebouncedCallback(() => {
        // `valueInDebounce` will always be late from the `valueInRender` for 1 character
        // is it possible to overcome somehow without explicit passing of the value?
        console.log({valueInDebounce: value})
    }, 1000)

    const decorateSetState = <T extends Function>(fn: T) => (args: any) => {
        fn(args)
        debounced() // passing args.target.value here is undesired
    }

    const setValueDecorated = decorateSetState(setValue)
    return (
      <div>
        <input onChange={setValueDecorated} value={value}/>
      </div>
    );
}

You can reproduce the issue in the https://stackblitz.com/edit/react-ts-d9udva

function type of param with useDebounce return a wrong value

example like

const a = () => 'I am a string'
const debouncedA = useDebounce(a, 500)
console.log(debouncedA)

log result: I am a string
expect: [function]

the reason is when useState got a function type of param, it will call this function to get result as state

export default function useDebounce<T>(
  value: T,
  delay: number,
  options?: { maxWait?: number; leading?: boolean; trailing?: boolean; equalityFn?: (left: T, right: T) => boolean }
): [T, ControlFunctions] {
  const eq = (options && options.equalityFn) || valueEquality;

  const [state, dispatch] = useState(value);
  ...

maybe to change like this will resolve?

export default function useDebounce<T>(
  value: T,
  delay: number,
  options?: { maxWait?: number; leading?: boolean; trailing?: boolean; equalityFn?: (left: T, right: T) => boolean }
): [T, ControlFunctions] {
  const eq = (options && options.equalityFn) || valueEquality;

  const [state, dispatch] = useState(typeof value === 'function' ? () => value : value);
  ...

Add documentation note that useEffect comparison is shallow

A use-case one might attempt is:

const [debouncedAddress] = useDebounce({
    address1,
    address2,
    country,
    city,
    state,
    zip,
}, 1500)

useEffect(() => {
    // do API call to update address on server
}, [
    debouncedAddress.address1
    debouncedAddress.address2
    debouncedAddress.country,
    debouncedAddress.city,
    debouncedAddress.state,
    debouncedAddress.zip,
])

...such that the form is auto-saved if the user has not typed in the last 1.5 seconds.

But in JS {} !== {} is true.

Suggesting the user use a hash function would be best. Otherwise, the above code example will trigger a re-render every 1500 ms. This should be accompanied w/ a note about the exhaustive-deps eslint rule.

useDebounce or throttle?

Am I wrong thinking useDebounce works more like useThrottle?

On first call, it immediately sets the value to the target value without debouncing.

I was trying to use it for a value which, when not falsy, will trigger an expensive calculation.

However, the calculation will be triggered immediately. Would it make sense to allow to provide a default like this

const [value] = useDebounce(fastValue, 200, defaultValue)

so that

`value = defaultValue

until the debounce triggers the change?

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)

Memoization

There is a heading in the documentation about memoization, but no example. I think you will need to allow a dependency array to be passed in, and make the usage the same as useCallback

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!

How to wait for the new value

Hey i wanted to handle user input from a textarea, but if the user submit the form before the debounce the previouse value is taken. How can i wait for the value to change then submit my form ?

Can someone help me ?

Memoize functions returned by useDebouncedCallback (i.e. by useCallback)

Let's say I have the same code as the one stated in current README of use-debounce:

  const [scrollHandler] = useDebouncedCallback(
    () => { setPosition(window.pageYOffset); },
    800,
    []
  );

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

But if I have eslint react exhaustive-deps rule rule enabled, it gives me the following warning:

image

To fix the eslint warning, I have to add the debounced function as a dependency of the useEffect:

  const [scrollHandler] = useDebouncedCallback(
    () => { setPosition(window.pageYOffset); },
    800,
    []
  );

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

But such code will return a new reference to scrollHandler on every render, thus forcing the effect to be subscribed and unsubscribed on each render.

So to fix it I actually have to do sth like:

  const [scrollHandler] = useDebouncedCallback(
    () => { setPosition(window.pageYOffset); },
    800,
    []
  );
  const memoizedScrollHandler = useCallback(scrollHandler, []);

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

Then everything works well.

But we could avoid requiring me to write this boilerplate of

const memoizedScrollHandler = useCallback(scrollHandler, [])

Instead, useDebouncedCallback could return functions that are already wrapped with useCallback, and they return a new function value only when some deps or delay is changed.

For example, it seems like cancelDebouncedCallback is already wrapped with useCallback ( https://github.com/xnimorz/use-debounce/blob/master/src/callback.js#L12 ).

WDYT?

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];
}

Docs about pending

I cannot see the reference for the pending method in the front page readme.
This would be great if you could also cover it.

Setting back to original value before debounce breaks.

If you set a value and set it back to its original value before the time, then it will use the first value.

For example:

function SomeComponent() {
	const [value, setValue] = useState('');
	const [debouncedValue] = useDebounce(value, 1000);

	return (
		<Fragment>
			<input type="text" value={value} onChange={({ target: { value: inputValue } }) => setValue(inputValue)} />
			{debouncedValue}
		</Fragment>
	);
}

If you type in "Hello", then delete it before the 1000ms, the debouncedValue will be "Hello" after 1000ms. This happens in general when you set it back to whatever it started as before the timeout.

v5.0.2 - cannot find module '[...]/node_modules/use-debounce/dist/index.js'

I'm getting this error in a TS + Next.js application after upgrading use-debounce to 5.0.2.
Things worked just fine prior to (and including) 5.0.1, so I'm suspecting it has something to do with these new lines that were introduced in package.json in one of the last commits:

"exports": {
    ".": {
      "import": "./dist/index.module.js",
      "require": "./dist/index.js"
    }
  }

...and the fact that there's no dist sub-folder created when installing the package; just lib and esm.
Removing the lines solved the problem for me.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.