React-select: Debouncing support

Created on 23 Nov 2015  Â·  30Comments  Â·  Source: JedWatson/react-select

Hi there, I was wondering there is a way to "debounce" async option loading with react-select. Currently, I'm seeing queries like this:

image

I would like react-select to only query after a 250ms typing pause. What do I need to do to make react-select do this? I have tried wrapping my function in a setTimeout, but that does not appear to do anything:

  taxons (input, callback) {
    setTimeout(function () {
      $.ajax({
        url: '/api/v2/taxons/search',
        dataType: 'json',
        cache: false,
        data: { name: input },
        success: function (response) {
          var options = response.data.map(function(taxon, index) {
            return (
              { value: taxon.id, label: taxon.attributes.tree_name }
            );
          });

          callback(null, {
            options: options
          })
        }
      })
    }, 250)
  }

Most helpful comment

A built-in debounce prop on the Async component would be a very welcome change.

All 30 comments

@radar You could replace your setTimeout call with Lodash's _.debounce(func, 250). Documentation at https://lodash.com/docs#debounce

Rather, _.throttle(func, 250) is probably more appropriate for this case.

(On my phone so bear with me)

IIRC, wrapping the asynchronous call in a denounce or throttle meant that the loading spinner remained after a page load. It's like whatever it is returning, react-select doesn't like it and so it shows the spinner instead.

Hopefully tomorrow I will get some time to get back to this code and give you some more hard data on it.

Just wanted to post this today so it doesn't look like I've fired and forgot.

On 24 Nov 2015, at 04:02, Jeremy Liberman [email protected] wrote:

Rather, _.throttle(func, 250) is probably more appropriate for this case.

—
Reply to this email directly or view it on GitHub.

That's a good point, the callback function can't really be throttled.
You could try switching to Promises
https://github.com/JedWatson/react-select#user-content-async-options-with-promises

And from there you can return the same promise for repeated calls. There are a number of options that come up when I search for "debounce promises" but I can't offer any specific recommendations for you since it's not something I've tried.

Thanks @MrLeebo, do you have an example of pairing that with debouncing?

You can use es6-promise-debounce.

<Select.Async
...
loadOptions={debounce(this.getItem, 500)}
... />

Has anyone got deBounce working?

@marcelometal version seems to lose the events, the function gets called but the menu is never populated. I assume the synthetic event is getting lost.

Any ideas?

Thanks.

@grandmore I've got debouncing working. The key is to make sure that lodash's debounce method is evaluated _exactly_ once.

Suppose you have a function fetch that retrieves the options for you and returns a Promise. You can then create a debounced version of this function like:

const debouncedFetch = debounce((searchTerm, callback) => {
  fetch(searchTerm)
    .then((result) => callback(null, {options: result})
    .catch((error) => callback(error, null));
}, 500);

This debouncedFetch function can then be passed to the Select.Async component:

<Select.Async
  ...
  loadOptions={debouncedFetch}
  ...
/>

Note that you cannot inline the debouncedFetch function in the render function of your component. If you do this, lodash's debounce would be called whenever the component is rerendered. This breaks the debounce function.

Hope this helps.

Thanks @irundaia this worked perfectly. Appreciate your help.

There is a missing bracket in your code, here is the corrected one for anyone else that needs it.

const debouncedFetch = _.debounce((searchTerm, callback) => {
  search(searchTerm)
    .then((result) => callback(null, { options: result }))
    .catch((error) => callback(error, null));
}, 500);

In case anyone runs into a similar use case to me:
I have three react-select fields within the same form and each of them had to run different debounced async functions (using lodash's debounce). I had to declare the component as a class and set the debounce in the constructor:

class TargetingPage extends Component {
  constructor(props) {
    super(props);
    this.fetchA = debounce(this.fetchA.bind(this), 450);
    this.fetchB = debounce(this.fetchB.bind(this), 450);
    this.fetchC = debounce(this.fetchC.bind(this), 450);
  }

  fetchA(query, callback) {
    const { customProp, searchFbGeoLocationTargets } = this.props;
    searchFbGeoLocationTargets(query, fbAccessToken, callback);
  }

  fetchB(query, callback) {
    const { fbAccessToken, searchFbLocales } = this.props;
    searchFbLocales(query, fbAccessToken, callback);
  }

  fetchC(query, callback) {
    const { fbAccessToken, searchFbInterests, adAccount } = this.props;
    searchFbInterests(query, fbAccessToken, callback, adAccount.providerParams.id);
  }

  render() {
    ...pass this.fetchA, this.fetchB and this.fetchC into onLoadOptions prop...
  }
}

Just to add to what @MrLeebo said, _.throttle works better for me in this case than _.debounce

Hi @irundaia !

Is debounce the same as _.debounce (lodash)?

@DanZeuss it is lodash’s debounce indeed.

hello on the docs there is no callback; why you do use it ?

const getOptions = (input) => {
  return fetch(`/users/${input}.json`)
    .then((response) => {
      return response.json();
    }).then((json) => {
      return { options: json };
    });
}

I have a doubt, how i can add a new parameter custolimized to getOptions ? i need to send the name of a table.

<Async
  name="form-field-name"
  value="one"
  loadOptions={getOptions}
/>

by example:

const getOptions = (input, tablename) => {
  return fetch(`/users/${input}.json`)
    .then((response) => {
      return response.json();
    }).then((json) => {
      return { options: json };
    });
}

I have a doubt, how i can add a new parameter custolimized to getOptions ? i need to send the name of a table.

let tablename='customer'

<Async
  name="form-field-name"
  value="one"
  loadOptions={getOptions(tablename)}
/>

but does not work... any idea?

@webmobiles This question isn't relevant to this issue at all. You should consider stack overflow or some other resource for learning JS. However, your function can return another function:

const generateOptionsFunc = tablename => input => {
  return fetch(`/${tablename}/${input}.json`)
    .then(res => res.json())
    .then(json => ({ options: json }))
}

// ...

const loadOptions = generateOptionsFunc("users")

// ...

<Async
  name="form-field-name"
  value="one"
  loadOptions={loadOptions}
/>

@MrLeebo thanks, i did not realize that. you saved my day, but that is a wrapper not? or decorator?

I think the term you're looking for is "higher-order function".

Is this really working for people? For you @grandmore ?
https://github.com/JedWatson/react-select/issues/614#issuecomment-244006496

A built-in debounce prop on the Async component would be a very welcome change.

@morgs32 I tried to use debounce and it keeps failing for me as well. The call is made according to the time out but the menu does not get populated and the loader continues to show in the screen. Where you able to figure out a workaround?

I have just gone with external loading of options for now instead of using the Async component. Debouncing of the function loading the option externally works fine.

@irundaia's and @grandmore's solutions are the way to go.

However, I faced one more issue before getting to the bottom of this:

When selecting any of the options from the async dropdown, an AJAX call with an empty query would then be dispatched no matter what. Same thing happens if a user clicks on the 'x' button on the right corner.

Please mind that this only occurs when using a debounce function.

After doing quite a bit of digging, I've learned that this issue is related to the fact that the options are resolved using the callback function which appears to have side effects when used with an empty input.

i.e. :
Without the debounce function we used to do this and things were rosy:

getOptions (input, callback) {
  if (!input) {
    return Promise.resolve({ options: [] });
  }
  // ...
}

By using the debounce function as suggested by @irundaia and @grandmore we were now forced to use the callback function instead:

getOptions (input, callback) {
  if (!input) {
    return callback(null, { options: [] });
  }
  //...
}

My solution:

Get the best of both worlds - Break down getOptions to input handling and AJAX handling so that the callback is used only when input is not empty. This is what I ended up using:

const debouncedFetch = _.debounce((searchTerm, callback) => {
  search(searchTerm)
    .then((result) => callback(null, { options: result }))
    .catch((error) => callback(error, null));
}, 500);

getOptions (input, callback) {
  if (!input) {
    return Promise.resolve({ options: [] });
  }
  debouncedFetch(input, callback);
}

Hope it helps someone in the future.

@kstratis that was helpful, I followed your approach to only debounce the fetching portion - that way, when the component mounts with an empty input (or I suppose a user sets the input to empty), I immediately return an empty options array (rather than debouncing that return as well).

Hello there! I was trying to use the debounce function inside the loadOptions prop of the ReactSelect.Async component. But I thing the typing for the callback function is wrong. The @types/react-select/lib/Async.d.ts defines:

loadOptions: (
    inputValue: string,
    callback: ((options: OptionsType<OptionType>) => void)) => Promise<any> | void;

Where OptionsType<OptionType> is OptionType[]

But I see everyone here using callback function like this:

callback(null, {options: OptionType[]})

Any help?

Hi!
I'm using a debounce hook that can do this.
Look at the demo.
This is the library.

@felipe-vicente The typing is correct. The examples from people in this issue are wrong (with the current version of react-select).

I want to share this hooks solution that I made because I wanted to avoid using third parties and my use case is simple, only one async select:

// manage in the state your current debounce (function, delay).
const [debounce, setDebounce] = useState({});

// Listen to changes of debounce (function, delay), when it does clear the previos timeout and set the new one.
useEffect(() => {
  const { cb, delay } = debounce;
  if (cb) {
    const timeout = setTimeout(cb, delay);
    return () => clearTimeout(timeout); 
  }
}, [debounce]);

// example of loadOptions 
const loadOptions = async (value, callback) => {
   setDebounce({
     cb: async () => {
       const data = await yourAsyncFetch(value);
       callback(data);
     },
     delay: 500 // ms
   });
}

Hope it helps ;).

Here is how I used loadash debounce for async calls in React hooks:

  const loadSuggestedOptions = React.useCallback(
    debounce((inputValue, callback) => {
      getOptions(inputValue).then(options => callback(options))
    }, 500),
    []
  );

Then call the function in loadoptions:

<AsyncSelect
   ...
   loadOptions={loadSuggestedOptions}
   ...
/>

@ayush-shta this totally works.. Just bear in mind that this is the callback version... I was mixing things up and my code was using the promises one and it wasn't working properly.

@ayush-shta This one saved me! I actually tried a bunch of different solutions to debounce an async API call to load the options, and this is the only one that worked properly and as expected. Really nice job.

Was this page helpful?
0 / 5 - 0 ratings