'Debouncing with multiple hooks

I am trying to implement debouncing in my app, however, the most I am able to achieve is to debounce the speed of the input. The gist of the App, is that it first takes input from the user, to generate a list of cities, according to the input and when selected, will provide a forecast for the whole week.

Here is the original code, without debouncing:

import React, { useState, useEffect } from 'react';
import * as Style from './Searchbar.styles';
import { weatherAPI } from '../API/api';
import endpoints from '../Utils/endpoints';
import { minCharacters } from '../Utils/minChars';
import MultiWeather from './MultiCard';
import useDebounce from '../Hooks/useDebounce';

export default function SearchBar() {
  const [cityName, setCityName] = useState('');
  const [results, setResults] = useState([]);
  const [chooseCityForecast, setChooseCityForecast] = useState([]);

  useEffect(() => {
    if (cityName.length > minCharacters) {
      loadCities();
    }
  }, [cityName, loadCities]);

  //this is used to handle the input from the user
  const cityValueHandler = value => {
    setCityName(value);
  }; 

  //first API call to get list of cities according to user input
  const loadCities = async () => {
    try {
      const res = await weatherAPI.get(endpoints.GET_CITY(cityName));
      setResults(res.data.locations);
    } catch (error) {
      alert(error);
    }
  }; 

  //second API call to get the forecast
  const getCurrentCity = async city => {
    try {
      const res = await weatherAPI.get(endpoints.GET_DAILY_BY_ID(city.id));
      console.log(res);
      setChooseCityForecast(res.data.forecast);
      setCityName('');
      setResults([]);
    } catch (error) {
      alert(error);
    }
  };
  
  return (
    <Style.Container>
      <h1>Search by City</h1>

      <Style.Search>
        <Style.SearchInner>
          <Style.Input type="text" value={cityName} onChange={e => cityValueHandler(e.target.value)} />
        </Style.SearchInner>
        <Style.Dropdown>
          {cityName.length > minCharacters ? (
            <Style.DropdownRow results={results}>
              {results.map(result => (
                <div key={result.id}>
                  <span
                    onClick={() => getCurrentCity(result)}
                  >{`${result.name}, ${result.country}`}</span>
                </div>
              ))}
            </Style.DropdownRow>
          ) : null}
        </Style.Dropdown>
      </Style.Search>
      {chooseCityForecast && (
        <section>
          <MultiWeather data={chooseCityForecast} />
        </section>
      )}
    </Style.Container>
  );
}

The code above works perfectly, aside from creating an API call everytime I add an additional letter. I have refered to this thread on implementing debouncing. When adjusted to my code, the implementation looks like this:

  const debounce = (fn, delay) => {
    let timerId;
    return (...args) => {
      clearTimeout(timerId);
      timerId = setTimeout(() => fn(...args), delay);
    }
  };

  const debouncedHandler = useCallback(debounce(cityValueHandler, 200), []);

But, as mentioned before, this results in debouncing/delaying user input by 200ms, while still creating additional API calls which each extra letter.

If I try to debounce the loadCities function or add a setTimeout method, it will delay the function, but will still make the API calls with each additional letter.

I have a hunch, that I need to remake the logic, which is handling the input, but at this point I am out of ideas.



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source