Reputation: 154
I'm creating a debounced tag search form which should fetch options and return searchResults
to feed loadOptions
.
Issue: because of the debounce, there is a consistent delay between the "correct" received options and the options displayed. The "correct options" are displayed on next call (one character minimum).
Idea (might not be the best): I would like to async/await loadOptions()
and wait for useSearchTags()
to return. Someone had the same issue there (https://github.com/JedWatson/react-select/issues/3145#issuecomment-434286534) and shared a solution. My case is a bit different as I am not directly fetching in loadOptions()
. Any idea?
https://codesandbox.io/s/debounce-react-select-loadoptions-tgud8?file=/src/App.js
// helpers/useDebouncedSearch.js
import { useState } from 'react';
import AwesomeDebouncePromise from 'awesome-debounce-promise';
import { useAsync } from 'react-async-hook';
import useConstant from 'use-constant';
import to from 'await-to-js';
const useDebouncedSearch = (searchFunction) => {
const [inputText, setInputText] = useState('');
const debouncedSearchFunction = useConstant(() =>
AwesomeDebouncePromise(searchFunction, 300)
);
const searchResults = useAsync(
async () => {
if (inputText.length === 0) {
return [];
} else {
let [err, debouncedResults] = await to(debouncedSearchFunction(inputText));
if(err) return [];
// reformat tags to match AsyncSelect config
const refactorTags = (tags) => {
return tags.map(tag => ({ label: tag.label, value: tag._id }))
}
return (debouncedResults.length !== 0) ?
refactorTags(debouncedResults) :
[];
}
},
[debouncedSearchFunction, inputText]
);
return {
inputText,
setInputText,
searchResults
};
}
export default useDebouncedSearch;
// SearchTags.js
import React, { useRef } from 'react';
import api from '../blablalivre-api.js';
import useDebouncedSearch from '../helpers/useDebouncedSearch.js';
import AsyncCreatableSelect from 'react-select/async-creatable';
import './SearchTags.scss';
const fetchTags = async text =>
(await api.searchTags(text));
const useSearchTags = () => useDebouncedSearch(text => fetchTags(text));
const SearchTagsRender = (props) => {
const { inputText, setInputText, searchResults } = useSearchTags();
const loadOptions = async (inputValue) => {
console.log('searchResults.result: ', searchResults.result);
return searchResults.result;
}
const handleOnChange = (tags) => {
props.updateTagsSelections(tags);
}
// issue AsyncCreatableSelect: https://github.com/JedWatson/react-select/issues/3412
return (
<AsyncCreatableSelect
isCreatable
isMulti
inputValue={inputText}
onInputChange={setInputText}
onChange={handleOnChange}
loadOptions={loadOptions}
cacheOptions
placeholder='Ajouter une thématique'
isClearable={false}
id='search-tags'
classNamePrefix='search-tags'
// to hide menu when input length === 0
openMenuOnClick={false}
// to remove dropdown icon
components={{ DropdownIndicator:() => null, IndicatorSeparator:() => null }}
// to custom display when tag is unknown
formatCreateLabel={inputValue => `Ajouter "${inputValue}"`}
// to reset focus after onChange = needs to user Refs
/>
);
};
export default SearchTagsRender;
Thanks a lot for your help!
Pierre
Upvotes: 2
Views: 4789
Reputation: 281656
The problem is in how loadOptions is being implemented in your case. loadOptions needs to provide AsyncSelect with a promise which resolves when the values are available. However when you use useAsync
to provide searchResults, it returns you a response with loading values initially and then a re-render causes it return the result when response are available
however, loadOptions in your case returns searchResults.result
which during the time of loading state is undefined
.
Now since the loadOptions
is resolved with undefined values, on next re-render it does't use the value unless the input is changed
The solution here is not not use useAsync
and provide searchResults
as a loadOptions function
const SearchTagsRender = props => {
const { inputText, setInputText, loaadSearchResults } = useSearchTags();
console.log(loaadSearchResults);
const handleOnChange = tags => {
const tagsFromForm = tags || [];
props.updateTagsFromForm(tagsFromForm);
};
return (
<>
<AsyncCreatableSelect
isCreatable
isMulti
inputValue={inputText}
onInputChange={setInputText}
onChange={handleOnChange}
loadOptions={loaadSearchResults}
cacheOptions
placeholder="Ajouter une thématique"
isClearable={false}
id="search-tags"
classNamePrefix="search-tags"
// to hide menu when input length === 0
openMenuOnClick={false}
// to remove dropdown icon
components={{
DropdownIndicator: () => null,
IndicatorSeparator: () => null
}}
// to custom display when tag is unknown
formatCreateLabel={inputValue => inputValue}
// to reset focus after onChange = needs to user Refs
/>
</>
);
};
export default SearchTagsRender;
const useDebouncedSearch = searchFunction => {
const [inputText, setInputText] = useState("");
const debouncedSearchFunction = useConstant(() =>
AwesomeDebouncePromise(searchFunction, 300)
);
const loaadSearchResults = async () => {
if (inputText.length === 0) {
return [];
} else {
let [err, debouncedResults] = await to(
debouncedSearchFunction(inputText)
);
if (err) return [];
console.log("debouncedResults: ", debouncedResults);
// reformat tags to match AsyncSelect config
const refactorItems = items => {
return items.map(item => ({
label: item.name,
value: item.alpha3Code
}));
};
return debouncedResults.length !== 0
? refactorItems(debouncedResults)
: [];
}
};
return {
inputText,
setInputText,
loaadSearchResults
};
};
Upvotes: 1