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 thecountryCode
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 thequery.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 thefetchCountryDetails
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 thesearchTerm
is not empty and call thefetchCountryDetails
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 thedelayDebounceFn
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 inuseQuery
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.