0

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.

mrvanagas
  • 37
  • 1
  • 9

1 Answers1

0

After some digging I found a simple solution, that will be refactored later, but the gist of it is to move the loadCities function inside the use effect and implenet useTimeout and clearTimeout methods within the useEffect and wrapping the API call function, as seen below:

useEffect(() => {
    let timerID;

    if (inputValue.length > minCharacters) {
      timerID = setTimeout(async () => {
        try {
          const res = await weatherAPI.get(endpoints.GET_CITY(inputValue));
          setResults(res.data.locations);
        } catch (error) {
          alert(error);
        }
      }, 900);
    }
    return () => {
      clearTimeout(timerID);
    };
  }, [inputValue]);

Hope this will help someone.

mrvanagas
  • 37
  • 1
  • 9