Issue #979

When dealing with user input, such as in an autocomplete component, it’s common to implement debouncing to reduce the number of API calls and improve the user experience.

React Query’s useQuery hook makes it easy to manage the state and lifecycle of API requests, and by combining it with debouncing, you can create a powerful and efficient data fetching solution.

In this tutorial, we’ll walk through an example of using useQuery with debouncing to fetch country details based on user input in an autocomplete component.

Setting up the custom Hook

Let’s start by creating a custom hook called useCountryDetails that encapsulates the logic for fetching country details:

import { useEffect, useState } from 'react';
import { useQuery } from '@tanstack/react-query';

const fetchCountryDetails = async (countryCode, signal) => {
  const response = await fetch(`/api/countries/${countryCode}`, { signal });
  return response.json();
};

export function useCountryDetails() {
  const [countryCode, setCountryCode] = useState('');
  const [callback, setCallback] = useState(null);

  const query = useQuery({
    queryKey: ['country-details', countryCode],
    queryFn: async ({ signal }) => {
      return fetchCountryDetails(countryCode, signal);
    },
    enabled: !!countryCode
  });

  useEffect(() => {
    if (callback) {
      callback(query.isFetching);
    }
  }, [query.isFetching, callback]);

  function fetchCountryDetails(code, cb) {
    setCountryCode(code);
    setCallback(() => cb);
  }

  return {
    countryDetails: query.data,
    fetchCountryDetails,
  };
}

In this hook:

  • We define a fetchCountryDetails function that makes an API call to fetch country details based on the provided countryCode.
  • We use the useQuery hook to manage the API request lifecycle.
  • The queryKey is set to include the countryCode to ensure that each request is cached separately.
  • The queryFn uses the fetchCountryDetails function and passes the signal parameter to enable cancellation of the request.
  • The enabled parameter is set based on the countryCode state, ensuring that the API call is only made when the countryCode is not empty.
  • We use the useEffect hook to update the loading indicator state whenever the query.isFetching value changes.
  • The fetchCountryDetails function updates the countryCode state and stores the provided callback function.
  • The hook returns the countryDetails from the useQuery response and the fetchCountryDetails function.

Since callback is a function, we need to set it properly, as the setState in React has this signature type SetStateAction<S> = S | ((prevState: S) => S);

Read more about useState

If you pass a function as initialState, it will be treated as an initializer function. It should be pure, should take no arguments, and should return a value of any type. React will call your initializer function when initializing the component, and store its return value as the initial state

Implementing Debouncing in the Autocomplete Component

Now, let’s create an Autocomplete component that uses the useCountryDetails hook and implements debouncing:

import { useCountryDetails } from './useCountryDetails';

const Autocomplete = () => {
  const { fetchCountryDetails, countryDetails } = useCountryDetails();
  const [searchTerm, setSearchTerm] = useState('');

  useEffect(() => {
    const delayDebounceFn = setTimeout(() => {
      if (searchTerm) {
        fetchCountryDetails(searchTerm, (isFetching) => {
          if (isFetching) {
            // Show loading indicator
          } else {
            // Hide loading indicator
          }
        });
      }
    }, 500);

    return () => clearTimeout(delayDebounceFn);
  }, [searchTerm, fetchCountryDetails]);

  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      {countryDetails && (
        <div>
          <h3>{countryDetails.name}</h3>
          <p>{countryDetails.capital}</p>
          <p>Population: {countryDetails.population}</p>
        </div>
      )}
    </div>
  );
};

In the Autocomplete component:

  • We use the useCountryDetails hook to access the fetchCountryDetails function and the fetched countryDetails.
  • We define a searchTerm state to store the user’s input.
  • We use the useEffect hook to implement debouncing:
  • We create a delayDebounceFn using setTimeout that will be executed after a 500-millisecond delay. Inside the delayDebounceFn, we check if the searchTerm is not empty and call the fetchCountryDetails function with the searchTerm.
  • We pass a callback function to fetchCountryDetails that updates the loading indicator state based on the isFetching value.
  • We return a cleanup function using clearTimeout to cancel the delayDebounceFn when the component is unmounted or when a new request is initiated.
  • We render an input field that updates the searchTerm state on change.
  • If countryDetails is available, we display the country name, capital, and population.

By combining useQuery with debouncing, you can enjoy several benefits:

  • Reduced API calls: Debouncing ensures that API requests are only made after a short delay, reducing the number of unnecessary calls and improving performance.
  • Cancellation of requests: The signal parameter in useQuery allows you to cancel requests when they are no longer needed, such as when the component is unmounted or when a new request is initiated.