loading...
Cover image for Debounced and Typesafe React Query with Hooks

Debounced and Typesafe React Query with Hooks

arnonate profile image Nate Arnold Updated on ・2 min read

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"?

Debounced and Typesafe React Query with Hooks

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;
}
Enter fullscreen mode Exit fullscreen mode

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
  });
}
Enter fullscreen mode Exit fullscreen mode

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 /> */}
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Discussion

pic
Editor guide
Collapse
ddsilva profile image
Daniel Silva

Great!

Thank you for sharing!