Pierre
Pierre

Reputation: 154

How to solve async search options delay?

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?

Codesandbox minimal example

https://codesandbox.io/s/debounce-react-select-loadoptions-tgud8?file=/src/App.js

Captures

On first character: no option displayed On second character: options from first character response displayed

Code

// 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

Answers (1)

Shubham Khatri
Shubham Khatri

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

Working demo

Upvotes: 1

Related Questions