React query with typescript and vite
Tags: react, typescript, api, query
Published: 2023-09-16 03:57:24

At the end of this article, you will understand
- What React-query is, why should you care about it, and when to use it.
- Whether it's a replacement for client-side state management like redux, zustand, and others.
- How to use react-query.

Let's get started.
**Why should I care about react-query**
Data fetching in react is usually done in the useEffect which involves a lot of boilerplate and has its downsides:
- Because effect gets to run after the component is rendered the fetching of data only starts once the rendering process is completed this delays the early fetching of data.
- Effect creates a network waterfall.
- Data fetching will be triggered every time your components unmount and mount unless you manually set up a data caching mechanism on the client which react-query provides for you out of the box.

React-query solves the downside listed above out-of-the-box without any complexity and also helps in data synchronization, deduplicating multiple requests for the same data into a single request. In a more general term, React-query is a library for data-fetching, data synchronization, and data caching.

**Does React-query replace redux, zustand, and others?**
For anyone trying to use this library for the first time, this is usually the question. React query helps to keep your server/remote state in sync with the client. An example could be fetching a list of users from the server which is asynchronous and keeping it in sync with the client. React-query helps us to manage this flow efficiently and in a very optimized manner. If your application requires moving parts where you need to track different client states then you could use any of the client state manager and react-query server state for remote state.
Having cleared this, congrats for making it this far, lets sip some wine and continue...

![Glass of wine](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r8epkqpx81wdeavb2wao.jpg)

We are going to build our simple app using `vite` to scaffold our React typescript project.

Firstly 

```
npm create vite@latest react-query --template react-ts
```
to create a react project with typescript template 
cd into react-query then run `npm i`

lets download all the dependencies we need

```
npm i @tanstack/react-query

npm i -D @tanstack/react-query-devtools
```
let's be a little bit organised by creating a `lib folder` and creating `reactQuery.ts` file
```
mkdir src/lib
cd src/lib
touch reactQuery.ts
```
inside reactQuery.ts add the content below

```ts
import { DefaultOptions, QueryClient } from "@tanstack/react-query";

const queryConfig: DefaultOptions = {
  queries: {
    refetchOnWindowFocus: import.meta.env.PROD, // *1
    retry: false,  // *2
    staleTime: 3000,  // line 3
  },
};

export const queryClient = new QueryClient({ defaultOptions: queryConfig });
```
On line *1 we are disabling the default refetchonWindowFocus to prevent auto fetching of data every time the window is on focus. In production its value is meant to be `true`; re-fetching will happen in the background to keep our state in sync or fresh.
NB: If refetchonWindowFocus is not disabled on development, you will continuously see data fetching when you open the browser development tool.

Line *2: retry; every failed query is retried a couple of times set on the config before the final error is shown. We are disabling the retry of queries.

Line 3 staleTime means how long does it take before a refetch happens. We could say is a 'freshness boundary', within this period any query instances with the same key will read from the cache because the data is still considered `fresh`.  Once the duration set is reached the query is considered `stale`. Stale queries are automatically re-fetched in the background.

Let's write our first `query`. Update the `main.tsx` with
`QueryClientProvider`

```ts
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { queryClient } from "./lib/reactQuery.ts";
import { QueryClientProvider } from "@tanstack/react-query";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
    {import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
      <App />
    </QueryClientProvider>
  </React.StrictMode>
);
```
Just like `react context provider` the  `QueryClientProvider` makes the `useQueryClient` available to its children and `ReactQueryDevtools` allows us to inspect stored queries and we are enabling it only on development mode(`import.meta.env.DEV`)

Next, 
```
mkdir src/api   // to create api folder
cd src/api
touch getProduct.ts // to create getProduct file, then paste the code below
```
our first query instance:

```ts
import { useQuery } from "@tanstack/react-query";
import { FetchError, fetchJSON } from "../lib/fetchJson";

export type Product = {
  id: string;
  description: string;
  title: string;
  thumbnail: string;
  brand: string;
  category: string;
  price: string;
  rating: number;
};

export type ProductResponse = {
  products: Product[];
  total: number;
};

export const getProductHandler = async ():Promise<Product[]> => {
  const response = await fetchJSON<ProductResponse>(
    "https://dummyjson.com/products"
  );
  return response.products;
};
export type queryConfigOption = {
  filter?: Record<string, string>;
  enabled?: boolean;
};
export const useGetProduct = ({ enabled=true }: queryConfigOption) => {
  return useQuery<Product[], FetchError>({
    queryFn: () => getProductHandler(),  // **1
    queryKey: ["products"], // **2
    enabled, // **3
  });
};
```

Line **1, `queryFn` is expecting a function that returns a promise with a well-defined type.
Line **2 `queryKey` we choose a key to use for the query instance. it is important to remember that when the `queryKey` value changes it triggers an automatic refetch.
on line **3 `enabled` by default is `true` which means the fetching is triggered immediately. we can use the `enabled` property to conditionally trigger if we want fetching to happen instantly. That means we could also set its value to be `false` until a certain requirement is met before we can trigger the fetching by setting `enabled: true`.

Next, let's make use of the useGetProduct hook we created

```ts
import "./App.css";
import { useGetProduct } from "./api/getProduct";
import { FetchError } from "./lib/fetchJson";

function App() {
  const productQuery = useGetProduct ({}); // *** 1

  if (productQuery.isLoading) {
    return <div>Loading...</div>;
  }

  if (productQuery.isSuccess && !productQuery.data?.length) {
    return <div>No Data</div>;
  }

  if (productQuery.isError) {
    return <div>{errorMessage(productQuery?.error)}</div>;
  }

  return (
    <section className="container">
      <div>
        <div className="product-list">
          {productQuery.data.map((product) => (
            <div className="product-list-item" key={product.id}>
              <img src={product.thumbnail} />
              <h4>{product.title}</h4>
              <p>{product.description}</p>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

const errorMessage = (error: Error) => {
  if (error instanceof FetchError) {
    return `${error.statusCode} : ${error.message} `;
  }
  return error.message;
};

export default App;

```
Line ***1 we could pass different options to the hooks, e.g. it could be dynamically setting `enabled` and also passing different filter parameters. Our query will be cached till the stale time.

To make any changes(or delete) on the server we could use the `useMutation` hook and then `invalidate` the query on success. Let's demonstrate it.
inside `api folder` create product `createProduct.ts`

```ts
import { useMutation } from "@tanstack/react-query";
import { fetchJSON } from "../lib/fetchJson";
import { queryClient } from "../lib/reactQuery";
import { Product } from "./getProduct";

export type CreateProductReq = {
  description: string;
  title: string;
  brand: string;
  price: string;
};

export const createProduct = async (data: CreateProductReq) => {
  const response = await fetchJSON<Product>(
    "https://dummyjson.com/products/add",
    {
      method: "Post",
      body: JSON.stringify(data),
      headers: {
        "Content-Type": "application/json",
      },
    }
  );
  return response;
};

export type queryConfigOption = {
  filter?: Record<string, string>;
  enabled?: boolean;
};

export const useCreateProduct = () => {
  return useMutation({ // **** 1
    mutationFn: createProduct, // **** 2
    onMutate: async (newProduct) => { // **** 3
      await queryClient.cancelQueries({ queryKey: ["products"] }); // **** 4

      const optimisticProduct = { id: Date.now().toString(), ...newProduct }; // **** 5

      const previousProduct = queryClient.getQueryData<Product[]>(["products"]); // **** 6

      queryClient.setQueryData(
        ["products"],
        [optimisticProduct, ...(previousProduct || [])]
      ); // **** 7

      return { previousProduct }; // **** 8
    },
    onSuccess: () => {
      queryClient.invalidateQueries(["products"]); // **** 9
      // showNotification("Product created successfully")
    },

    onError: (error, payload, context) => {
      if (context?.previousProduct) {  // **** 10
        queryClient.setQueryData(["products"], context?.previousProduct);
      }
    },
  });
};
```
Let's walk through the lines of code. Any desired changes on the server are done using the `useMutation` hook as shown on line  **** 1. `useMutation` provides us with different helper functions that we could use to make any changes during the mutation lifecycles.
Line **** 2 `mutationFn` references our server mutating function which is asynchronous.
Line **** 3 `onMutate` allows us to make changes to our cache query while waiting for the mutation lifecycle to finish. It provides us with a function handler to perform something like an optimistic update.
inside the `onMutate` handler.
Line ***4 and line ***5, we are canceling all current product queries and composing a new product object for our optimistic update using the newProduct variable which is our payload respectively.

Line **** 6, we are retrieving the already stored products query with queryKey `products` which will be used for a rollback in case of error.
Line **** 7, we are updating the product query with the newly composed product object. This implies that the user will see an updated product list with a new product while an update is still happening on the server, if there is an error on the server the product list is rolled back.

Line **** 8, `previousProduct` is passed to the context which will be used for rollback in case of error.

Line **** 9, if the request was successful, `onSuccess` gets to run and the existing `products` query is invalidated which triggers an automatic refetch. Once the refetch is done our product query is updated with fresh data from the server.
if the request to the server fails `onError` gets to execute and we are rolling back the optimisitically updated products query with previousProduct that was passed to the context on line **** 8.
Let's make use of the `useCreateProduct` hook we created by updating our App.tsx

```ts
import "./App.css";
import { useGetProduct } from "./api/getProduct";
import { FetchError } from "./lib/fetchJson";
import { useInput } from "./hook/useInput";
import { useCreateProduct } from "./api/createProduct";

function App() {
  const { onChange, values } = useInput({
    title: "",
    description: "",
    price: "",
    brand: "",
  });

  const productQuery = useGetProduct({});
  const createProductMutation = useCreateProduct(); // ***** 1

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    await createProductMutation.mutateAsync(values); // ***** 2
  };

  if (productQuery.isLoading) {
    return <div>Loading...</div>;
  }

  if (productQuery.isSuccess && !productQuery.data?.length) {
    return <div>No Data</div>;
  }

  if (productQuery.isError) {
    return <div>{errorMessage(productQuery?.error)}</div>;
  }

  return (
    <section className="container">
      <div>
        <div className="wrapper">
          <form onSubmit={handleSubmit}>
            <div className="input-container">
              <div className="input-field">
                <label htmlFor="title">Title</label>
                <input
                  name="title"
                  type="text"
                  id="title"
                  value={values.title}
                  onChange={onChange}
                />
              </div>
              <div className="input-field">
                <label htmlFor="description">Description</label>
                <input
                  name="description"
                  type="text"
                  id="description"
                  value={values.description}
                  onChange={onChange}
                />
              </div>
              <div className="input-field">
                <label htmlFor="price">Price</label>
                <input
                  name="price"
                  type="text"
                  id="prices"
                  value={values.price}
                  onChange={onChange}
                />
              </div>
              <div className="input-field">
                <label htmlFor="brand">Brand</label>
                <input
                  name="brand"
                  type="text"
                  id="brand"
                  value={values.brand}
                  onChange={onChange}
                />
              </div>
              <div className="submit-wrap">
                <button type="submit">
                  {createProductMutation.isLoading ? "Submitting" : "Add product"}
                </button>
              </div>
            </div>
          </form>
        </div>

        <div className="product-list">
          {productQuery.data.map((item) => (
            <div className="product-list-item" key={item.id}>
              <img src={item.thumbnail} />
              <h4>{item.title}</h4>
              <p>{item.description}</p>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

const errorMessage = (error: Error) => {
  if (error instanceof FetchError) {
    return `${error.statusCode} : ${error.message} `;
  }
  return error.message;
};

export default App;

```
Line ***** 1 we are calling the `useCreateProduct` which returns an object that contains a mutation handler `mutate`  and `mutateAsync` for updating the server state. it is important to remember that `mutate` and `mutateAsync` are asynchronous. 
On line ***** 2  we are triggering the mutation handler `mutateAsync` with the desired payload.
NB: Updating and deleting of records could  have the same flow.

Other helper functions

```ts
// lib/fetchJson.ts
export class FetchError extends Error {
  response: Response;
  statusCode: number;
  constructor({
    message,
    response,
    statusCode,
  }: {
    message: string;
    response: Response;
    statusCode: number;
  }) {
    super(message);

    this.name = "FetchError";
    this.statusCode = statusCode;
    this.response = response;
    this.message = message;
  }
}
export const fetchJSON = async <JSON = unknown>(
  input: RequestInfo,
  init?: RequestInit
): Promise<JSON> => {
  const response = await fetch(input, init);

  const data = await response.json();

  if (response.ok) {
    return data;
  }

  throw new FetchError({
    message: response.statusText,
    response,
    statusCode: response.status,
  });
};

```

```ts
// hook/userInput.ts
import { useState } from "react";
import { CreateProductReq } from "../api/createProduct";

export const useInput = (defaultValue: CreateProductReq) => {
  const [values, setValues] = useState(defaultValue);

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValues((prev) => ({ ...prev, [e.target.name]: e.target.value }));
  };

  return { onChange, values };
};

```

Conclusion:
React-query saves us a lot of boilerplate required for managing server state and caching of data. it also tries to keep our client state in sync with our server state. With React-query setup in our system, all asynchronous states all automatically managed and where are left with little client state to manage with React context or zustand if needed.

References
https://tanstack.com/query/v4/docs/react/typescript
https://tanstack.com/query/v4/docs/react/guides/queries
https://tanstack.com/query/v4/docs/react/guides/mutations

[Github link](https://github.com/solomonfrank/react-query)