Debounced and Typesafe React Query with Hooks
Tags: react, reactquery, axios, typescript
Published: 2020-09-23 18:31:03

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](https://dev-to-uploads.s3.amazonaws.com/i/g1jqpz9cjigz0xajx66n.png)

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.

```typescript
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.

```typescript
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!).

```typescript
// 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 /> */}
    </>
  );
};
```