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
useQueryhook to manage the API request lifecycle. - The
queryKeyis set to include thecountryCodeto ensure that each request is cached separately. - The
queryFnuses 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
useEffecthook to update the loading indicator state whenever thequery.isFetchingvalue changes. - The
fetchCountryDetailsfunction updates the countryCode state and stores the provided callback function. - The hook returns the
countryDetailsfrom the useQuery response and thefetchCountryDetailsfunction.
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
useCountryDetailshook to access the fetchCountryDetails function and the fetched countryDetails. - We define a
searchTermstate to store the user’s input. - We use the
useEffecthook to implement debouncing: - We create a delayDebounceFn using setTimeout that will be executed after a 500-millisecond delay.
Inside the
delayDebounceFn, we check if thesearchTermis not empty and call thefetchCountryDetailsfunction with the searchTerm. - We pass a callback function to
fetchCountryDetailsthat updates the loading indicator state based on the isFetching value. - We return a cleanup function using
clearTimeoutto cancel thedelayDebounceFnwhen the component is unmounted or when a new request is initiated. - We render an input field that updates the
searchTermstate on change. - If
countryDetailsis 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
signalparameter inuseQueryallows you to cancel requests when they are no longer needed, such as when the component is unmounted or when a new request is initiated.
Start the conversation