Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React-Select Async loadOptions is not loading options properly

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.

import React from 'react';
import AsyncSelect from 'react-select/lib/Async';
import Typography from '@material-ui/core/Typography';
import i18n from 'react-intl-universal';

const _ = require('lodash');

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}
      />
    );
  }
}

export default SearchableSelect;

Edit to response Steve's answer

Thank you for your answer Steve. Still no luck. I try to response according to your response points.

  1. If i don't use optionsValue, rather use getOptionValue and getOptionLevel then query result don't loaded properly. I mean there blank options loaded, no text value.
  2. yes you are right, is a synchronous method returning a string, i don't need to override this. And this working fine and noOptionsMessage shows properly. Thanks to point this out.
  3. actionOnSelectedOption is not a noop method, its may have some responsibility to perform. I try to use SearchableSelect as an independent component, if i need some back-end action to do this function will trigger that accordingly. For example, i use this in my project's user-profile, where user can update his school/college information from existing entries. When user select an option there is a profile update responsibility to perform.
  4. Yes you are right. I don't need to maintain inputValue in state, thanks.
  5. I do make sure defaultOptions is an array.
  6. I do test without using debounce, still no luck. i am using debounce to limit the backend call, otherwise there may a backend call for every key-stroke that surely i don't want.

async select work perfectly for 2/3 queries and after that it suddenly stop working. One distinguishable behaviour i observe that for those cases search indicators ( ... ) also not showing.

Thank you so much for you time.

Edit 2 to response Steve's answer

Thank you so much for your response again. I was wrong about getOptionValue and getOptionLabel. If loadOptions got response both these function called. So i removed my helper optionsValue function from my previous code snippet and update my code-snippet according to ( In this post also ). But still no luck. In some cases async-select didn't work. I try to take a screenshot one such case. I do name use in my local-db name "tamim johnson" but when i search him i didn't get any response but got proper response back from back-end. Here is the screenshot of this case tamim johnson

I not sure how clear this screenshot is. Tamim johnson also in 6th position in my ranklist.

Thank you sir for your time. I have no clue what i am doing wrong or missing something.

Edit 3 to response Steve's answer

This is preview tab response for user search named "tamim johnson".

preview tab

like image 544
Shakil Avatar asked Oct 25 '18 07:10

Shakil


2 Answers

The issue is that Lodash's debounce function is not suitable for this. 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 the debounced loadOptions prop function is actually returning the last func invocation, and so the "real" promise we care about is never subscribed to.

Instead use a promise-returning debounce function

For example:

import debounce from "debounce-promise";

//...
this.getOptions = debounce(this.getOptions.bind(this), 500);

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

like image 83
craigmichaelmartin Avatar answered Sep 20 '22 09:09

craigmichaelmartin


I found out that people intend to look for this problem. So i am posting my update portion of code that fix the issue. Converting from async-await to normal callback function fix my issue. Special thanks to Steve and others.

import React from 'react';
import AsyncSelect from 'react-select/lib/Async';
import { loadingMessage, noOptionsMessage } from './utils';
import _ from 'lodash';

class SearchableSelect extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      selectedOption: this.props.defaultValue
    };
    this.getOptions = _.debounce(this.getOptions.bind(this), 500);
  }

  handleChange = selectedOption => {
    this.setState({
      selectedOption: selectedOption
    });
    if (this.props.actionOnSelectedOption) {
      this.props.actionOnSelectedOption(selectedOption.value);
    }
  };

  mapOptionsToValues = options => {
    return options.map(option => ({
      value: option.id,
      label: option.name
    }));
  };

  getOptions = (inputValue, callback) => {
    if (!inputValue) {
      return callback([]);
    }

    const { searchApiUrl } = this.props;
    const limit =
      this.props.limit || process.env['REACT_APP_DROPDOWN_ITEMS_LIMIT'] || 5;
    const queryAdder = searchApiUrl.indexOf('?') === -1 ? '?' : '&';
    const fetchURL = `${searchApiUrl}${queryAdder}search=${inputValue}&limit=${limit}`;

    fetch(fetchURL).then(response => {
      response.json().then(data => {
        const results = data.results;
        if (this.props.mapOptionsToValues)
          callback(this.props.mapOptionsToValues(results));
        else callback(this.mapOptionsToValues(results));
      });
    });
  };

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

export default SearchableSelect;
like image 44
Shakil Avatar answered Sep 21 '22 09:09

Shakil