Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Async Debounce for Select 2.0 not behaving as expected #3075

Closed
Steague opened this issue Sep 26, 2018 · 12 comments
Closed

Async Debounce for Select 2.0 not behaving as expected #3075

Steague opened this issue Sep 26, 2018 · 12 comments

Comments

@Steague
Copy link

Steague commented Sep 26, 2018

As the title says, lodash (_.debounce) implementation is not working for Select 2.0 async component.

It looks like the caching mechanism is caching the previous input and displaying that in the results.

Here is my forked code example:
Edit react-codesandboxer-example

Does anyone have a working debounce example that works for the 2.0 implementation of React Select Async?

@craigmichaelmartin
Copy link

Great reproduction!

It doesn't appear to me that this issue has to do with the cache.

The issue

It seems to me the issue is with an inaccurate expectation of how Lodash's debounce works.

Lodash specifies that

subsequent calls to the debounced function return the result of the last func invocation

Not that:

subsequent calls return promises which will resolve to the result of the next func invocation

This means each call which is within the wait period to our debounced loadOptions prop function is actually returning the last func invocation, and so the "real" promise we care about is never subscribed to.

An example

Here is a forked example making it less react-select specific (removes the cacheOptions and defaultOptions props, passes the option {leading: true} to the debounce function, and makes the wait period longer):

https://codesandbox.io/s/olmjz7mn9z

User types a and waits past the debounce wait period (1 second):

  1. Async#handleInputChange is called, with a new value of a.
  2. Within this, Async#loadOptions is called and subscribes to the promise returned from the passed in loadOptions fn (which is our _.debounced fn).
  3. Our _.debounced fn returns a promise (thanks to {leading: true} option) which resolves to the correct data (eg, the list of Alabama, Alaska, America Samoa).

User then types l, then a, then s, then k, then a (all within the debounce wait period). We now have an input showing Alaska, but with the dropdown showing both Alabama and Alaska. Here's why:

  1. Async#handleInputChange is called, with a new value of al. Within this, Async#loadOptions is called and subscribes to the promise returned from the passed in loadOptions fn (which is our _.debounced fn), which is a promise which resolves to the correct data (both Alabama and Alaska).
  2. Async#handleInputChange is called, with a new value of ala. Within this, Async#loadOptions is called and subscribes to the promise returned from the passed in loadOptions fn (which is our _.debounced fn), which (because this event is within the wait period) returns the "result of the last func invocation", which is the promise from loadOptions from when al was typed in.
  3. Step two is repeated for alas, alask, and finally alaska each time with our debounced fn (because we're still within the debounce wait period) returning the "result of the last func invocation" which is the promise from loadOptions from when al was typed in. (This promise resolves to both Alabama and Alaska.)
  4. After having finished typing, the wait period finally expires from when alaska was typed, and our debounced function now actually calls our true loadOptions function, which returns a promise resolving to the correct list of just Alaska. However, nothing is subscribed to this promise and it has no effect. Remember, when our word, alaska, was finished being typed in, our debounced loadOptions function still being within the wait period, returned the "result of the last func invocation" (the promise from loadOptions for the al) and the Async#loadOptions code, having received a fine value), used it and moved on.

A solution

Using a promise-returning debounce method where subsequent calls return promises which will resolve to the result of the next func invocation.

Updated example using debounce-promise:
https://codesandbox.io/s/98vxxr18zw

@mmuller99
Copy link

@craigmichaelmartin Thanks for the clear explanation. Using { leading: true } does mean that the first character entered will always result in a call to loadOptions as shown in the timeline illustration here. So only the subsequent characters will be debounced

@kimmoxter
Copy link

@craigmichaelmartin
Awesome job, this really works like a charm. I tried others solutions and this one it's perfect.
Well done! .

@zirho
Copy link

zirho commented May 27, 2019

https://codesandbox.io/s/react-select-async-debounce-example-9uo88
with v3.0.3

@agwells
Copy link

agwells commented Jun 28, 2019

Another approach, that does seem to work with lodash's debounce, is to invoke the callback param React-Select pases to onLoad, and don't return a Promise.

  constructor(props) {
    super(props);

    const wait = 1000; // milliseconds
    const loadOptions = (inputValue, callback) => {
      this.getAsyncOptions(inputValue)
        .then(results => callback(results))
      // Explicitly not returning a Promise.
      return;
    }
    this.debouncedLoadOptions = _.debounce(loadOptions, wait);
  }

The tricky part is that it's easy to accidentally return a promise when you're calling an async search function, if you use an arrow function or an async/await function.

@dabyland
Copy link

@craigmichaelmartin Worked perfectly for my use-case. Thanks for the detailed post and sandbox!

@yufanw
Copy link

yufanw commented Aug 15, 2019

    super(props);

    const wait = 1000; // milliseconds
    const loadOptions = (inputValue, callback) => {
      this.getAsyncOptions(inputValue)
        .then(results => callback(results))
      // Explicitly not returning a Promise.
      return;
    }
    this.debouncedLoadOptions = _.debounce(loadOptions, wait);
  }

Using this solution, if you load options and change your input without selecting anything, it will make the second call but the subsequent results will not be handled or show up on the options list.

@agwells
Copy link

agwells commented Aug 18, 2019

Using this solution, if you load options and change your input without selecting anything, it will make the second call but the subsequent results will not be handled or show up on the options list.

Huh, I'm not experiencing that problem in my project. Maybe there's some subtlety in my particular use-case that makes it work.

Well, if the technique I described doesn't work for you, then I suggest going with the approach craigmichaelmartin described, using debounce-promise.

@yufanw
Copy link

yufanw commented Aug 19, 2019

We decided to use a combination of lodash/debounce and Creatable instead of AsyncCreatableSelect. This allows us to handle all the async stuff on my own component state instead of letting the Select handle it internally with all its quirks.

@Shaker-Hamdi
Copy link

Shaker-Hamdi commented Jan 13, 2020

Hello guys, I'm using react hooks and I'm using an async redux action to get the data when the user starts searching. It works fine and all, but the debouncing doesn't work. I'm guessing because I'm doing an async action that returns a promise.
How can I solve this? Does anyone have this issue?

@abdulfatai360
Copy link

Hello guys, I'm using react hooks and I'm using an async redux action to get the data when the user starts searching. It works fine and all, but the debouncing doesn't work. I'm guessing because I'm doing an async action that returns a promise.
How can I solve this? Does anyone have this issue?

Try debounce-promise NPM package.

@bladey
Copy link
Contributor

bladey commented May 28, 2020

Hello -

In an effort to sustain the react-select project going forward, we're closing old issues.

We understand this might be inconvenient but in the best interest of supporting the broader community we have to direct our efforts towards the current major version.

If you aren't using the latest version of react-select please consider upgrading to see if it resolves any issues you're having.

However, if you feel this issue is still relevant and you'd like us to review it - please leave a comment and we'll do our best to get back to you!

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests