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}
/>
);
}
}
I answered your question on StackOverflow.
Thank you so much @cutterbl for your response. Still no luck, I edited my question in stackoverflow for your answer response, here
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}
/>
);
}
}
@cutterbl ok, I tried that and got this error
TypeError: selectOptions.some is not a function[Learn More] react-select.esm.js:4689
isValidNewOption
react-select.esm.js:4689
componentWillReceiveProps
react-select.esm.js:4775
callComponentWillReceiveProps
react-dom.development.js:12399
updateClassInstance
react-dom.development.js:12612
updateClassComponent
react-dom.development.js:14267
beginWork
react-dom.development.js:15106
performUnitOfWork
react-dom.development.js:17903
workLoop
react-dom.development.js:17944
callCallback
react-dom.development.js:147
invokeGuardedCallbackDev
react-dom.development.js:196
invokeGuardedCallback
react-dom.development.js:250
replayUnitOfWork
react-dom.development.js:17224
renderRoot
react-dom.development.js:18037
performWorkOnRoot
react-dom.development.js:18919
performWork
react-dom.development.js:18826
performSyncWork
react-dom.development.js:18799
requestWork
react-dom.development.js:18676
scheduleWork
react-dom.development.js:18480
enqueueSetState
react-dom.development.js:12143
./node_modules/react/cjs/react.development.js/Component.prototype.setState
react.development.js:386
Async/_this.handleInputChange/</<
react-select.esm.js:4557
I thought that the upgrade docs said that loadoptions is unchanged
but instead it expect a list instead of dict.
loadOptions expects a method that either a) returns a Promise, or b) has a callback defined in it's arguments. Ultimately, your method must return/resolve to an array of your options.
thanks @cutterbl for answering this, closing this as the initial query has been resolved.
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
I am also facing this issue, data is getting from API can see in the attached file but not displaying in component's list
I tried the above solutions but none of them worked in my case
Here is my code
`class SearchAbleComp extends Component {
constructor(props, context) {
super(props, context);
this.state = {}
}
loadOptions = (inputValue, callback) => {
if (!inputValue) {
callback([]);
} else {
const { apiCall } = this.props.actions;
setTimeout(() => {
apiCall(inputValue).then((res) => {
console.log(res.options);
callback(res.options);
});
}, 1000);
}
};
render() {
const { assignedSdCards } = this.state;
return (
</div>
);
}
}
export default SearchAbleComp;
`
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 thecallback
version ofloadOptions
, 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} /> ); } }
Hello
Sorry but I am encountering the exactly same issue.
I have followed @cutterbl answer and defined a loadOptions method for distant options data fetch and the xhr call won't ever be triggered.. I don't understand how such a classic use case is not working out of the box..
here is my code:
`import React, { useState } from 'react';
import AsyncSelect from 'react-select';
import { getCities } from 'Apis';
const CityAutocomplete = ({ onChange, name, placeholder, value }) => {
const onThisChange = e => {
console.log((e))
onChange(e);
}
const loadOptions = async (inputValue) => {
console.log(inputValue) // never prints
const res = await getCities(inputValue); // = axios.get('cities/search', { params: { query: inputValue } })
return res;
};
return (
name={name}
placeholder={placeholder}
selectedOption={value}
loadOptions={loadOptions}
/>)
}`
I don't kn ow why the component's return content disappears in the preview.. here is the content:
'<'AsyncSelect
onChange={onThisChange}
name={name}
placeholder={placeholder}
selectedOption={value}
loadOptions={loadOptions}
'/>'
Hey @Cedoriku and anyone else who might be running into this issue.
I recently found out why it was happening.
Your import of import AsyncSelect from 'react-select';
needs to be changed to
import AsyncSelect from 'react-select/async';
Once I did that, it worked as expected
Most helpful comment
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 returnundefined
(since there was no previous value from the underlying function). This leavesreact-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