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.
- isPending: true when we fetch new data, but when the data comes from the cache, it will be false.
- isFetching: true when we fetch new data, and when the data comes from the cache, it will be true.
- isLoading: true when we fetch new data for the very first time.
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), }); // ... }