Search code examples
reactjssetstate

ReactSearchAutocomplete - set state not updating items, only updates on next search


I'm using ReactSearchAutocomplete to display a list of 10 suggested names from a mySql query. The query is performed with a 100ms delay on user input, and it works fine but I'm clearly not handling the state properly because the query will return results fine, but upon calling setState, the search bar does not provide suggestions as would be expected.

Only if I then continue typing and trigger another query do the search results show up, and they seemingly are the results of the previous query.

My question is, how do I get the autocomplete component to properly render the suggested results from my query only AFTER setting the state of the namesList?

const SearchBar = () => {
const [namesList, setNamesList] = useState([]);

const handleOnSearch = (searchTerm) => {
  if (searchTerm.length < 2) {
    return;
  }

  let queryString = "url/api/search/" + searchTerm;
  if (isDev) {
    queryString = "http://localhost:3001/search/" + searchTerm;
  }

  fetch(queryString)
    .then((response) => response.json())
    .then((json) => {
      let searchResults = json.map((o) => ({ id: o.name, name: o.name }));
      setNamesList(searchResults);
    })
    .catch((error) => console.error(error));
};

  return (
    <div className="search-bar">
      <ReactSearchAutocomplete
        items={namesList}
        onSearch={handleOnSearch}
        autoFocus={true}
        inputDebounce={100}
        showNoResults={false}
      />
    </div>
  );
};

export default SearchBar;

Solution

  • The issue is that the library you are using does not allow for dynamic data. This is one of their earliest issues. They will not be working towards it. The whole library was built keeping static requirements in mind.

    You have the option to use some other library MUI autocomplete. They support server calls.

    You can also make your own component, if the requirement is not that complex.

    Basically what you are supposed to have is a:

    1. a controlled input field
    2. list of divs to map over the items
    3. logic to run API call in denounced manner
    const debounceWait = 1000; //debounce time, you can keep it inside the component too
    const AutoCompleteDropdown = () => {
      const [query, setQuery] = useState("");
      const [options, setOptions] = useState([]);
      const ref = useRef(query); //ref to match query with input
      const debounceRef = useRef(null); //ref to cancel existing timeouts for the fetch
      const onSearch = (e) => {
        let string = e.target.value;
        setQuery(string);
      };
    
      useEffect(() => {
        ref.current = query; //updating ref for the query
        clearTimeout(debounceRef.current);
        if (query.length < 2) {
          setOptions([]);
        } else {
          debounceRef.current = setTimeout(() => {
            clearTimeout(debounceRef.current);
            fetch(`https://dummyjson.com/posts/search?q=${query}`)
              .then((res) => res.json())
              .then((json) => {
                if (ref.current === query) //this check is necessary in case one server call started earlier finishes later then the other server call
                  setOptions(json.posts.map((x) => x.title));
              })
              .catch((error) => console.error(error));
          }, debounceWait);
        }
      }, [query]);
      console.log({ options });
      return (
        <div className="dropdown">
          <div className="control">
            <div className="selected-value">
              <input
                type="text"
                value={query}
                name="searchTerm"
                onChange={onSearch}
              />
            </div>
          </div>
    
          <div>
            {options.map((option, index) => {
              return <div>{option}</div>;
            })}
          </div>
        </div>
      );
    };
    

    Here is a demo


    The above example should work for your use case. There is an inefficiency that API calls are not being cancelled. But that is not a direct requirement you have mentioned so I have not worked towards it.