React useQuery : A Complete Guide

September 18, 2024 (2mo ago)

React is a powerful library for building UI components, but when it comes to managing server-side data, things can get tricky. This is where React Query comes into play. React Query is a tool that simplifies data fetching, caching, and synchronization in your React apps. At the heart of it is the React useQuery hook, which helps you fetch and manage data effortlessly.

Let's dive into the useQuery hook and see how it can make your life easier when working with server-side data in React.

What is React useQuery?

React useQuery is a React hook provided by the React Query library (now called TanStack Query). It allows you to fetch data from an API or any external source in a declarative way. But what sets it apart from other data-fetching methods like fetch or axios is that it provides out-of-the-box support for caching, automatic re-fetching, and background data synchronization.

Getting Started with useQuery

To use the useQuery hook, you need to install the React Query library first. You can do this by running the following command:

npm i @tanstack/react-query

Then, wrap your app in the QueryClientProvider so that the useQuery hook can access the React Query context.

import React from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from "./App";
 
const queryClient = new QueryClient();
 
function Root() {
  return (
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  );
}
 
export default Root;

Basic Example of useQuery

Let’s say we want to fetch a list of users from a placeholder API. Here’s how you can use React useQuery for that:

import React from "react";
import { useQuery } from "@tanstack/react-query";
 
const fetchUsers = async () => {
  const response = await fetch("https://jsonplaceholder.typicode.com/users");
  return response.json();
};
 
function UsersList() {
  const { data, error, isLoading } = useQuery(["users"], fetchUsers);
 
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error loading users</div>;
 
  return (
    <ul>
      {data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
 
export default UsersList;

How it works:

Handling Cache and Refetching

One of the key features of React useQuery is its built-in caching mechanism. By default, it caches the data for a specific query key and automatically re-fetches it when needed. The next time you call the same query, it will return cached data instantly without refetching, unless you specifically tell it to.

You can also set the stale time, which is the time before the cache is considered outdated. Here’s how you can control the re-fetching behavior:

const { data, isLoading } = useQuery(
  ["users"],
  fetchUsers,
  { staleTime: 5000 } // data will be fresh for 5 seconds
);

This means that if a query is called within 5 seconds of the initial fetch, it won’t hit the server again. It’ll use the cached data.

Polling or Background Data Synchronization

Another useful feature of React useQuery is the ability to poll the server at regular intervals to keep the data up-to-date. You can set the refetch interval to achieve this:

const { data, isLoading } = useQuery(
  ["users"],
  fetchUsers,
  { refetchInterval: 10000 } // refetch every 10 seconds
);

With this option, useQuery will automatically re-fetch the data every 10 seconds, keeping it in sync with the server.

Handling Errors

Handling errors in data fetching is critical for providing a good user experience. React useQuery provides an easy way to handle errors, as shown in the previous examples. You can access the error object to display an appropriate message when things go wrong.

An Example for error handling:

const { data, error, isError, isLoading } = useQuery(["users"], fetchUsers);
 
if (isLoading) {
  return <div>Loading...</div>;
}
 
if (isError) {
  return <div>Error: {error.message}</div>;
}
 
return (
  <ul>
    {data.map((user) => (
      <li key={user.id}>{user.name}</li>
    ))}
  </ul>
);

Optimistic Updates

Optimistic updates are a way to update the UI optimistically before the server responds. This can make your app feel more responsive and improve the user experience. React Query provides a way to achieve this using the optimistic option.

Key Concepts in Optimistic Updates

Example of Optimistic Updates

Let’s walk through a practical example where a user updates their profile information, such as their name:

Step 1: Define a mutation function that updates the user’s name:

const updateUser = async (newUserData) => {
  const response = await fetch(`/api/updateUser`, {
    method: "POST",
    body: JSON.stringify(newUserData),
  });
 
  if (!response.ok) {
    throw new Error("Failed to update user");
  }
 
  return response.json(); // Return the updated user data
};

Step 2: Use the useMutation hook to perform the update:

import { useMutation, useQueryClient } from "@tanstack/react-query";
 
function UserProfile({ user }) {
  const queryClient = useQueryClient();
 
  const mutation = useMutation(updateUser, {
    // Optimistically update the UI
    onMutate: async (newUserData) => {
      // Cancel any ongoing queries for this user to prevent them from overwriting the optimistic update
      await queryClient.cancelQueries(["user", user.id]);
 
      // Save the current user data to rollback in case of an error
      const previousUserData = queryClient.getQueryData(["user", user.id]);
 
      // Optimistically update the cache with the new user data
      queryClient.setQueryData(["user", user.id], (oldUserData) => ({
        ...oldUserData,
        ...newUserData, // Apply new user data
      }));
 
      // Return the rollback data so we can revert on error
      return { previousUserData };
    },
 
    // If the mutation fails, rollback the optimistic update
    onError: (error, newUserData, context) => {
      // Rollback to the previous user data
      queryClient.setQueryData(["user", user.id], context.previousUserData);
    },
 
    // If the mutation succeeds, invalidate and refetch the user data
    onSuccess: () => {
      queryClient.invalidateQueries(["user", user.id]); // Refetch fresh data
    },
  });
 
  // Function to handle form submission
  const handleUpdate = (newUserData) => {
    mutation.mutate(newUserData);
  };
 
  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={() => handleUpdate({ name: "New Name" })}>
        Update Name
      </button>
    </div>
  );
}

Breakdown of What’s Happening:

onMutate:

onError: If the request to the server fails, we restore the previous state using the data saved in context.previousUserData. This ensures the UI reflects the correct data if the update was unsuccessful.

onSuccess: When the server successfully responds, queryClient.invalidateQueries(['user', user.id]) triggers a re-fetch of the user data, ensuring the cache and UI are updated with the actual data from the server.

Customizing Query Behavior

React Query provides a way to customize the behavior of queries using query options. Here are some common options you can use:

Conclusion

In this guide, we’ve covered the basics of using the React useQuery hook in React Query. We’ve seen how it simplifies data fetching, caching, and synchronization in React apps. By leveraging the power of React useQuery, you can build fast, responsive, and reliable applications that handle server-side data effortlessly.

This is just the tip of the iceberg when it comes to React Query. There are many more features and options available to help you manage your data effectively. You can read the official documentation to learn more about it.