I recently worked on a project with a search input that loaded results from an external API. The basic problem is simple: User enters search text > component displays a list of results. If you have built one of these before though, you know it's not as easy as it sounds. How do we ensure that a search for "React" doesn't also turn into a search for "R", "Re", "Rea", "Reac" and "React"?
The answer lies in debouncing the fetch calls to the API to give the user time to stop typing. I looked for a lot of solutions to this problem using React Query, and pieced together a couple hooks that work really well together to achieve the desired "debounced query" result I was looking for.
Setup
The following packages are needed to follow along (assuming you are already using a newish version of React in your project):
react-query
axios
typescript
Hooks
Create 2 files for your hooks:
useDebounce.ts
This file creates a custom hook that will set a timeout delay on updating state (in this case, to wait on user input). If the timeout exists, it will clear it as well.
import React from "react";
export default function useDebounce(value: string, delay: number = 500) {
const [debouncedValue, setDebouncedValue] = React.useState(value);
React.useEffect(() => {
const handler: NodeJS.Timeout = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
useReactQuery.ts
This file creates a custom hook that will accept our query arguments and return a React Query useQuery
hook, wrapping an axios.get()
, which will hopefully return a Promise with data from our getStuff
function.
import { useQuery } from "react-query";
import axios from "axios";
export type QueryResponse = {
[key: string]: string
};
const getStuff = async (
key: string,
searchQuery: string,
page: number
): Promise<QueryResponse> => {
const { data } = await axios.get(
`https://fetchurl.com?query=${query}&page=${page}`
);
return data;
};
export default function useReactQuery(searchQuery: string, page: number) {
return useQuery<QueryResponse, Error>(["query", searchQuery, page], getStuff, {
enabled: searchQuery, // If we have searchQuery, then enable the query on render
});
}
Consumption
Container.tsx
That's basically it! Now all we have to do is go to our container component and put the hooks to work! Notice we are passing the searchQuery
into our debounce hook and passing the result of the debounce to our React Query hook and responding to changes in data or fetching status. You can activate the React Query dev tools and see the resultant queries run in real time (pretty sweet!).
// import { ReactQueryDevtools } from "react-query-devtools";
import useDebounce from "../../hooks/useDebounce";
import useReactQuery from "../../hooks/useReactQuery";
export type ContainerProps = {
searchQuery: string;
isFetchingCallback: (key: boolean) => void;
};
export const Container = ({
searchQuery,
isFetchingCallback,
}: Readonly<ContainerProps>): JSX.Element => {
const debouncedSearchQuery = useDebounce(searchQuery, 600);
const { status, data, error, isFetching } = useReactQuery(
debouncedSearchQuery,
page
);
React.useEffect(() => isFetchingCallback(isFetching), [
isFetching,
isFetchingCallback,
]);
return (
<>
{data}
{/* <ReactQueryDevtools /> */}
</>
);
};
Top comments (4)
Great!
Thank you for sharing!
Why not to use paginatedQuery instead of useQuery?
paginatedQuery is now outdated, nah ??
Nice Work 👍
But now it takes the same debounce delay even if the data is available in query cache. have any idea to avoid it?
Please share...