question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

React-Select Async loadOptions is not loading options properly

See original GitHub issue

React Async Select loadoption sometimes fail to loads the option. This is a very strange phenomenon after couple of set of queries react loadoptions don’t load any value but i can see from log that results properly came from backend query. My codebase is totally up to date with react-select new release and using

“react-select”: “^2.1.1”

Here is my front end code for react-async select component. I do use debounce in my getOptions function to reduce number of backend search query. This should not cause any problem i guess. I would like to add another point that i observe in this case, loadoptions serach indicator ( … ) also not appear in this phenomenon.

class SearchableSelect extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      inputValue: '',
      searchApiUrl: props.searchApiUrl,
      limit: props.limit,
      selectedOption: this.props.defaultValue
    };
    this.getOptions = _.debounce(this.getOptions.bind(this), 500);
    //this.getOptions = this.getOptions.bind(this);
    this.handleChange = this.handleChange.bind(this);
    this.noOptionsMessage = this.noOptionsMessage.bind(this);
    this.handleInputChange = this.handleInputChange.bind(this);
  }

  handleChange(selectedOption) {
    this.setState({
      selectedOption: selectedOption
    });
    if (this.props.actionOnSelectedOption) {
      // this is for update action on selectedOption
      this.props.actionOnSelectedOption(selectedOption.value);
    }
  }

  handleInputChange(inputValue) {
    this.setState({ inputValue });
    return inputValue;
  }

  async getOptions(inputValue, callback) {
    console.log('in getOptions'); // never print
    if (!inputValue) {
      return callback([]);
    }
    const response = await fetch(
      `${this.state.searchApiUrl}?search=${inputValue}&limit=${
        this.state.limit
      }`
    );
    const json = await response.json();
    console.log('results', json.results); // never print
    return callback(json.results);
  }

  noOptionsMessage(props) {
    if (this.state.inputValue === '') {
      return (
        <Typography {...props.innerProps} align="center" variant="title">
          {i18n.get('app.commons.label.search')}
        </Typography>
      );
    }
    return (
      <Typography {...props.innerProps} align="center" variant="title">
        {i18n.get('app.commons.errors.emptySearchResult')}
      </Typography>
    );
  }
  getOptionValue = option => {
    return option.value || option.id;
  };

  getOptionLabel = option => {
    return option.label || option.name;
  };

  render() {
    const { defaultOptions, placeholder } = this.props;
    return (
      <AsyncSelect
        cacheOptions
        value={this.state.selectedOption}
        noOptionsMessage={this.noOptionsMessage}
        getOptionValue={this.getOptionValue}
        getOptionLabel={this.getOptionLabel}
        defaultOptions={defaultOptions}
        loadOptions={this.getOptions}
        placeholder={placeholder}
        onChange={this.handleChange}
      />
    );
  }
}

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:12 (3 by maintainers)

github_iconTop GitHub Comments

15reactions
craigmichaelmartincommented, Dec 28, 2018

The issue is that Lodash’s debounce function is not suitable for this purpose, since subsequent calls to Lodash’s debounced function returns the value of underlying function’s previous value, and not a promise which will resolve to the underlying function’s next invocation value.

This means (with {leading: true} not passed to debounce, as in @shakilaust’s code), the first typings into the input within the wait period all return undefined (since there was no previous value from the underlying function). This leaves react-select in an infinite loading state. After the wait period, any new typing into the input within the wait period again all return the underlying function’s previous value (as lodash’s decounce is spec’ed to do), but in this case that value is now the promise from when the underlying function was called with the previous typing input. Thus, react-select is always one wait period value behind.

See an example of this here: https://codesandbox.io/s/oxovwo4ojy

For a full explanation see https://github.com/JedWatson/react-select/issues/3075#issuecomment-450194917

13reactions
cutterblcommented, Oct 30, 2018

For those finding this issue later, the problem was in how async/await was used for the getOptions method. Async/await returns a Promise, while in the code above he was trying to utilize the callback version of loadOptions, mixing implementations. This is easily fixed by making minor adjustments to the code.

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import AsyncSelect from 'react-select/lib/Async';
import debounce from 'lodash.debounce';
import noop from 'lodash.noop';
import i18n from 'myinternationalization'; // whatever i18n library it is you're using

const propTypes = {
  searchApiUrl: PropTypes.string.isRequired,
  limit: PropTypes.number,
  defaultValue: PropTypes.object,
  actionOnSelectedOption: PropTypes.func
};

const defaultProps = {
  limit: 25,
  defaultValue: null,
  actionOnSelectedOption: noop
};

export default class SearchableSelect extends Component {
  static propTypes = propTypes;
  static defaultProps = defaultProps;
  constructor(props) {
    super(props);
    this.state = {
      inputValue: '',
      searchApiUrl: props.searchApiUrl,
      limit: props.limit,
      selectedOption: this.props.defaultValue,
      actionOnSelectedOption: props.actionOnSelectedOption 
    };
    this.getOptions = debounce(this.getOptions.bind(this), 500);
    this.handleChange = this.handleChange.bind(this);
    this.noOptionsMessage = this.noOptionsMessage.bind(this);
    this.handleInputChange = this.handleInputChange.bind(this);
  }

  getOptionValue = (option) => option.id; // maps the result 'id' as the 'value'

  getOptionLabel = (option) => option.name; // maps the result 'name' as the 'label'

  handleChange(selectedOption, {action}) { // you can use the 'action' to do different things here
    this.setState({
      selectedOption: selectedOption
    });
    // this is for update action on selectedOption
    // will use the noop defaultProp if the dev didn't define the prop, so no need to conditionally call
    this.state.actionOnSelectedOption(selectedOption.value);
  }

  // async/await returns a Promise, so use the Promise form as seen in the
  // documentation https://react-select.com/async
  async getOptions(inputValue) {
    if (!inputValue) {
      return [];
    }
    const response = await fetch(
      `${this.state.searchApiUrl}?search=${inputValue}&limit=${this.state.limit}`
    );
    const json = await response.json();
    return json.results;
  }

  // inputValue state is controlled in the Select, so this probably isn't necessary
  // except to maybe validate that it is changing
  handleInputChange(inputValue) {
    this.setState({ inputValue });
    return inputValue;
  }

  // as long as `i18n.get()` is synchronous, returning a string, there's no need to override the
  // entire Component
  noOptionsMessage(inputValue) {
    if (this.props.options.length) return null;
    if (!inputValue) {
      return i18n.get('app.commons.label.search');
    }
    return i18n.get('app.commons.errors.emptySearchResult');
  }

  render() {
    const { defaultOptions, placeholder } = this.props;
    const { selectedOption } = this.state;
    return (
      <AsyncSelect
        cacheOptions
        value={selectedOption}
        noOptionsMessage={this.noOptionsMessage}
        getOptionValue={this.getOptionValue}
        getOptionLabel={this.getOptionLabel}
        defaultOptions={defaultOptions}
        loadOptions={this.getOptions}
        placeholder={placeholder}
        onChange={this.handleChange}
      />
    );
  }
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

React-Select Async loadOptions is not loading options properly
Here is my front end code for react-async select component. I do use debounce in my getOptions function to reduce number of backend...
Read more >
React-Select Async loadOptions is not loading options ...
I found out that people intend to look for this problem. So i am posting my update portion of code that fix the...
Read more >
Async - React Select
Use the Async component to load options from a remote source as the user types. import Async, { useAsync } from 'react-select/async'; ...
Read more >
React-Select loadOptions conditionally - CodeSandbox
Activating extension 'vscode.typescript-language-features' failed: Could not find bundled tsserver.js.
Read more >
Load options on the first open of the Async drop down menu
Reactjs – React-Select Async loadOptions is not loading options properly ... I found out that people intend to look for this problem. So...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found