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:
Does anyone have a working debounce example that works for the 2.0 implementation of React Select Async?
Great reproduction!
It doesn't appear to me that this issue has to do with the cache.
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.
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):
Async#handleInputChange
is called, with a new value of a
.Async#loadOptions
is called and subscribes to the promise returned from the passed in loadOptions
fn (which is our _.debounced fn).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:
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).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.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.)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.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
@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
@craigmichaelmartin
Awesome job, this really works like a charm. I tried others solutions and this one it's perfect.
Well done! .
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.
@craigmichaelmartin Worked perfectly for my use-case. Thanks for the detailed post and sandbox!
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.
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
.
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.
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?
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.
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!
Most helpful comment
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
Not that:
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):Async#handleInputChange
is called, with a new value ofa
.Async#loadOptions
is called and subscribes to the promise returned from the passed inloadOptions
fn (which is our _.debounced fn).User then types
l
, thena
, thens
, thenk
, thena
(all within the debounce wait period). We now have an input showingAlaska
, but with the dropdown showing both Alabama and Alaska. Here's why:Async#handleInputChange
is called, with a new value ofal
. Within this,Async#loadOptions
is called and subscribes to the promise returned from the passed inloadOptions
fn (which is our _.debounced fn), which is a promise which resolves to the correct data (both Alabama and Alaska).Async#handleInputChange
is called, with a new value ofala
. Within this,Async#loadOptions
is called and subscribes to the promise returned from the passed inloadOptions
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 fromloadOptions
from whenal
was typed in.alas
,alask
, and finallyalaska
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 fromloadOptions
from whenal
was typed in. (This promise resolves to both Alabama and Alaska.)alaska
was typed, and our debounced function now actually calls our trueloadOptions
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 debouncedloadOptions
function still being within the wait period, returned the "result of the last func invocation" (the promise fromloadOptions
for theal
) and theAsync#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