Managing remote data using react query.

March 17, 2023

TanStack query is library that simplifies the way we fetch, cache and synchronize data from the server. TanStack query can be used for multiple library and frameworks like react, vue, svelte and even react native 😲. We can also control when to refetch the data on certain conditions like refetch when the window is focused again. In this blog i am going to show how to fetch and mutate data from the server using react query.

But before let's see how we typically used to get the data from the server without using react query or other packages for data fetching.

import { useState, useEffect } from 'react';
 
export const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);
      try {
        const response = await fetch(url);
        if (!response.ok) throw new Error(response.statusText);
        const json = await response.json();
        setIsLoading(false);
        setData(json);
        setError(null);
      } catch (error) {
        setError(`${error} Could not Fetch Data `);
        setIsLoading(false);
      }
    };
    fetchData();
  }, [url]);
  return { data, isLoading, error };
};

The custom useFetch hook is okay just for fetching it gives us nice loading, or error state but there are many other problems that this custom hooks cannot handle:

So to solve this problem react query comes. There are other packages out there to handle these like RTK query, SWR but we are going to use react query because i feel TanStack query is more powerful in terms of control over other packages.

Installation

yarn add @tanstack/react-query

React query is like a module of tanstack query since tanstack query can be used for multiple library and frameworks, there are collection of modules for each library or framework like @tanstack/react-query for react @tanstack/vue-query for vue and so on.

Query Client Provider

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 
const ONE_MIN = 60 * 1000;
 
const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: ONE_MIN } },
});
 
function App() {
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}

To perform fetching, mutation and use the query client first we need to initailize the query client with the query client provider. We can pass some defaultOptions to query client if noting is passed on query client options than the default value will be kept for example if we do not specify the staleTime then it will set as 0 meaning the query will immediately refetch when it mounts. Similary there is property refetchOnWindowFocus as the name says it will refetch on the window focus after comming to the window after another tab or so by default refetchOnWindowFocus is set to true. You can check other options by checking the docs.

Fetching the data

import { useQuery } from '@tanstack/react-query';
import { getBookings } from '../../services/api';
 
export function useBookings() {
  const { data: bookings, isLoading } = useQuery({
    queryKey: ['bookings'],
    queryFn: getBookings,
    staleTime: 0, // we can override the stale time from the query client.
  });
 
  return { bookings, isLoading };
}
// api.js your api looks something like this.
export const getBookings = async function () {
  const response = await api.get(`/bookings`, {
    headers: {
      'Content-Type': 'application/json',
    },
  });
 
  return response?.data;
};
// BookingTable.jsx
function BookingTable() {
  const { bookings, isLoading } = useBookings();
 
  if (isLoading) return <Spinner />;
 
  if (!bookings.length) return <Empty resourceName="bookings" />;
 
  // render component with data accordingly
}
 
export default BookingTable;

useQuery function is used to fetch the data and control some options like refetchOnWindowFocus, retry, number of retries and more. The best part of react query is caching the data for example if we use this useBookings hook in multiple components it will not hit the API call multiple times but only ones, that is all handled by the queryKey that we pass to useQuery hook. The 'bookings' string is the unique identifier for this useBookings hook. So if the query key remains the same data is returned from the cache 😍.

Dynamically fetching the data

import { useQuery } from '@tanstack/react-query';
import { getBooking } from '../../services/api';
import { useParams } from 'react-router-dom';
 
export function useBooking() {
  const { bookingId } = useParams();
 
  const { data: booking, isLoading } = useQuery({
    queryKey: ['booking', bookingId],
    queryFn: () => getBooking(bookingId),
    retry: false,
  });
 
  return { booking, isLoading };
}

There will be situations where we need to fetch data dynamically for example based on a id of a booking. The data will be different depending on the id of the booking so to fetch the data again when the id of the booking changes we simply pass the changing value in the queryKey the queryKey does not needs to be string it can hold value thats why we have the bracket notion. Notice the bookingId which is the value for the queryKey changes, as soon the bookingId changes the another API call is made. Its like a dependency in a useEffect hook when the value changes the function remounts in this case.

Devtools

yarn add @tanstack/react-query-devtools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
 
<QueryClientProvider client={queryClient}>
  <ReactQueryDevtools initialIsOpen={false} />
  ...
</QueryClientProvider>;

To use the react query devtools we need to add the @tanstack/react-query-devtools dependency and pass inside the QueryClientProvider. The react query devtools is the most handy devtools that i used while developing the application unlike react devtools the component tree will be so huge that its gets hard to see the tree and find the component when i need to see the state and props however i like the profiler tab to see the chart and nice message why the component rerender or stayed the same across multiple renders.

react query devtools

Developers can see the state of all queries and mutation in a single dashboard kind of thing. Developers can see the fresh data, stale data, inactive data, loading state. also the mutation can be filtered we can toggle the wifi, delete query from cache and more.

Mutating the data.

import { useMutation } from '@tanstack/react-query';
import { createBooking } from '../../services/api';
import toast from 'react-hot-toast';
 
export function useCreateBooking() {
  const { mutate: create, isLoading: isCreating } = useMutation({
    mutationKey: ['create-booking'],
    mutationFn: (dataObj) => updateBooking(dataObj),
    onSuccess: (data) => {
      toast.success(`Booking created successfully !`);
    },
    onError: () => {
      toast.error('There was an error creating booking');
    },
  });
 
  return { create, isCreating };
}

useMutation hook is used to change the data in the server, in useQuery the queryFn used to be the get method but in useMutation the mutationFn must be post, patch or delete method. We also get onSuccess, onError methods to show some nice notification and also debug from the console.

// createBookingForm.jsx
import { useCreateBooking } from '../../hooks/bookings/useCreateBooking';
 
function createBookingForm() {
  const [bookingData, setBookingData] = useState('');
  const { isCreating, create } = useCreateBooking();
 
  function handleCreateBooking(e) {
    e.preventDefault();
    mutate(
      { bookingData },
      // we can access the onSuccess and onError methods where we call the mutate fn
      {
        onSuccess: () => {
          // we can use this method for resetting some states.
        },
      },
    );
  }
 
  return (
    <form onSubmit={handleCreateBooking}>
      <input
        type="text"
        name="bookingData"
        onChange={(e) => setBookingData(e.target.value)}
      />
      <button type="submit">create</button>
    </form>
  );
}
 
export default createBookingForm;

We just need to simply call the mutate method when the submit event is triggered and rest is handled by useMutation. However i see lot of developers resetting the form or resetting the state after mutate function which is very bad ux because the mutation can fail too, so you should always use the onSuccess and onError methods avaliable in the mutate function.

// not to do this way.
function handleCreateBooking(e) {
  e.preventDefault();
  mutate({ bookingData });
  setBookingData('');
}

Synchronizing

Imaging When we perform a mutation in a table then the result is always new or the data is stale, for example when we delete one booking from the db, the list of booking data is new. So without react query we have to manually reload the browser and get the synchronized data. In react query we can use so called invalidateQueries method to synchronize data.

import { useMutation, useQueryClient } from '@tanstack/react-query'; // get the queryClient that we specify in provider.
import { createBooking } from '../../services/api';
import toast from 'react-hot-toast';
 
export function useCreateBooking() {
  const queryClient = useQueryClient(); // can use the queryClient with this hook.
 
  const { mutate: create, isLoading: isCreating } = useMutation({
    mutationKey: ['create-booking'],
    mutationFn: (dataObj) => updateBooking(dataObj),
    onSuccess: (data) => {
      toast.success(`Booking created successfully !`);
      queryClient.invalidateQueries({
        queryKey: ['bookings'], // the query key that we want to synchronize after new data is added.
      });
    },
    onError: () => {
      toast.error('There was an error creating booking');
    },
  });
 
  return { create, isCreating };
}

Removing queries

There might be sitautions where we donot want any query to remain in cache. for example we dont want to store the user that just loggoed out from the application so TanStack team also thought about those sitautions, to implement this we use the removeQueries method.

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { logout as logoutApi } from '../../services/api';
import { useNavigate } from 'react-router-dom';
import toast from 'react-hot-toast';
 
export function useLogout() {
  const navigate = useNavigate();
  const queryClient = useQueryClient();
 
  const { mutate: logout, isLoading } = useMutation({
    mutationKey: ['logout'],
    mutationFn: logoutApi,
    onSuccess: () => {
      queryClient.removeQueries(); // removes all queries from the cache.
      //  remove queries from the cache based on their query keys.
      // queryClient.removeQueries({ queryKey, exact: true });
      navigate('/login', { replace: true });
    },
    onError: (err) => toast.error(err.message),
  });
 
  return { logout, isLoading };
}

Prefetching

With react query we can prefetch the data they need before it's needed If this is the case, you can use the prefetchQuery method to prefetch the results of a query to be placed into the cache.

Assuming there is pagination in both API and client side a user is seeing the 1st page in bookings table and the user clicks on next page which is 2nd page, normally there will be a new get request for the 2nd page so user has to wait until it gets download but we can go a step ahead by pre fetching the data of next and prev page and keeping in the cache while user is looking at the data of 2nd page. prefetchQuery function will download the page 1st and 3rd page so that will result a great user experience by removing those anoying loading indicators.

// useBooking.js
if (pageNum < pageCount)
  queryClient.prefetchQuery({
    queryKey: ['bookings', pageNum + 1],
    queryFn: () => getBookings({ pageNum: pageNum + 1 }),
  });
if (pageNum > 1)
  queryClient.prefetchQuery({
    queryKey: ['bookings', pageNum - 1],
    queryFn: () => getBookings({ pageNum: pageNum - 1 }),
  });

That's it for this blog guys, of course i have not covered everything about tanstack query there is a lot of stuffs to look through in tanstack query but if you want to know other topics like Query filters, inifiteQueries for infinite scrolling apps and more you can check out the offical tanstack docs and implement in your code 👋.

References

If you liked it share it with your friends or colleague