React Query: The King of Data Fetching

Installation and setup

npm i @tanstack/react-query

Initialize the QueryClient

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";

import { App } from "@components";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </StrictMode>
);

Basic Usage

One hook to rule them API calls.

import { ProductCard } from "@components";
import styles from "./products-list.module.sass";
import { useQuery } from "@tanstack/react-query";
import { Product } from "@types";

export function ProductsList() {
    const { data: products, isLoading, error } = useQuery({
        queryKey: ["products", page], // we need to pass the name and arguments of the function to let @tanstack be able to cache the data
        queryFn: () => fetchProducts(page),
    });

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

    if (error) {
        return <div>Error: {error.message}</div>;
    }

    return (
        <div className={styles.productsList}>
        {products?.map((product) => (
            <ProductCard key={product.id} product={product} />
        ))}
        </div>
    );
}

const PRODUCTS_ENDPOINT = "http://127.0.0.1:3000/products";
const fetchProducts = async (page: number): Promise<Product[]> => {
  const response = await fetch(`${PRODUCTS_ENDPOINT}?page=${page}`);
  const data = await response.json();
  return data;
};

Most important data you can use from useQuery return values

useQuery is returning a list of useful data to use in your component like isError, isLoading, isPending, refetch function...

To see the full list of data you can return from useQuery, you can check the official documentation.

isLoading, isFetching and isPending

Keep in mind some differences between isLoading, isFetching and isPending.

Call useQuery conditionally - yes! no problem!

You can call useQuery conditionally by using the enabled option.

import { ProductCard } from "@components";
import styles from "./products-list.module.sass";
import { useQuery } from "@tanstack/react-query";
import { Product } from "@types";

export function ProductsList() {
    const { data: products, isLoading, error } = useQuery({
        queryKey: ["products", page],
        queryFn: () => fetchProducts(page),
        enabled: !!page, // this will prevent the query from being executed if page is not defined
    });

    // ...
}

Custom hooks for isolate, organize and reuse your queries

Sometimes you will end up with a little messy in your component regarding the complexity of the query options or because you have several queries in the same component. Beside that, you will want to reuse the same query in different components.

In this scenario, is a great idea to create a custom hook to have that fetch logic reusable and modular.

import { useCallback, useEffect } from "react";
import { Product } from "@types";
import { useQuery } from "@tanstack/react-query";

const PRODUCTS_ENDPOINT = "http://127.0.0.1:4000/";

export function useFetchProducts() {
  const {
    data: products,
    isPending,
    error,
  } = useQuery({
    queryKey: ["products"],
    queryFn: () => handleFetchProducts(),
  });

  const handleFetchProducts = useCallback(async (): Promise<Product[]> => {
    const response = await fetch(PRODUCTS_ENDPOINT);
    return await response.json();
  }, []);

  return { products, isPending, error };
}

useSuspenseQuery

useSuspenseQuery guarantees that the data won't never be undefined. That means with a little change in our custom hook, to use useSuspenseQuery instead of useQuery, we can have a better error handling.

import { useSuspenseQuery } from "@tanstack/react-query";
// ...

export function useFetchProducts() {
  const {
    // now, data will be resolved before returning the value
    data: products,
    isPending,
    error,
  } = useSuspenseQuery({
    queryKey: ["products"],
    queryFn: () => handleFetchProducts(),
  });

  // ...

}

So, you can use useSuspenseQuery if you are working with data that will be always defined and there is no undefined case.

But, the main scenario to use useSuspenseQuery is we are working with useSuspense React hook.

useSuspense and useQuery

Imagine that the component from before

import { Suspense } from "react";

export function ExperienceLayout() {
  return (
      <Suspense fallback={<div>Loading...</div>}>
        {/* Is there is any component that is using useSuspenseQuery it will render the fallback conmponent */}
        <ProductsList />
      </Suspense>
  );
}

Multiple queries

You can use multiple queries in the same component without

import { useQueries } from "@tanstack/react-query";

export function ProductsList() {
  const [products, experiences] = useQueries({
    queries: [
      {
        queryKey: ["products"],
        queryFn: () => fetchProducts(),
      },
      {
        queryKey: ["experiences"],
        queryFn: () => fetchExperiences(),
      },
    ],
  });

  // ...
}

Multiples queries depeding on each other

You can use multiple queries depeding on each other with 2 main patterns:

Using the enabled option

import { useQueries } from "@tanstack/react-query";

export function ProductsList() {
  const { data: products, isLoading: isLoadingProducts, error: errorProducts } = useQuery({
    queryKey: ["products"],
    queryFn: () => fetchProducts(),
  });


  // second query with the random id
  // this query need to wait for the first query to finish
  // for that, we can use the `enabled` option
  const randomId = Math.floor(Math.random() * products.length);
  const { data: experiences, isLoading: isLoadingExperiences, error: errorExperiences } = useQuery({
    queryKey: ["experiences", randomId],
    queryFn: () => fetchExperiences(randomId),
    enabled: !!products,
  });

  // ...
}

Using useSuspenseQuery in the first query

import { useSuspenseQuery } from "@tanstack/react-query";

export function ProductsList() {
  const { data: products, isLoading: isLoadingProducts, error: errorProducts } = useSuspenseQuery({
    queryKey: ["products"],
    queryFn: () => fetchProducts(),
  });


  // here, we are sure that the data is already fetched and never will be undefined 
  const randomId = Math.floor(Math.random() * products.length);
  const { data: experiences, isLoading: isLoadingExperiences, error: errorExperiences } = useQuery({
    queryKey: ["experiences", randomId],
    queryFn: () => fetchExperiences(randomId),
  });

  // ...
}