Harshal Ranjhani
Posted on September 18, 2024
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:
useQuery(['users'], fetchUsers)
: This hook triggers the fetchUsers function, which fetches the data from the API. The ['users'] is the query key used to identify this specific query.isLoading
: A boolean value that indicates whether the query is still fetching the data.data
: The resolved data from the query (in this case, a list of users).error
: If an error occurs during the fetching process, it’s caught here.
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
Immediate UI feedback: When the user performs an action (e.g., clicking a "Like" button), the UI is updated right away, even though the request to the server might still be pending.
Rollback on failure: If the server request fails (e.g., due to a network error or invalid data), the UI is rolled back to its previous state, so the user doesn't see incorrect data.
Optimism and Pessimism: Optimistic updates assume success (optimism), and only correct the UI if something goes wrong (pessimism).
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
:
- Cancel any ongoing queries:
queryClient.cancelQueries(['user', user.id])
ensures that any other requests related to the user’s data are stopped, so they don’t interfere with the optimistic update. - Save the previous state:
queryClient.getQueryData(['user', user.id])
retrieves the current data, which will be used in case we need to roll back. - Apply optimistic update:
queryClient.setQueryData(['user', user.id], ...)
updates the cache with the optimistic data. In this case, it updates the user’s name in the cache immediately, so the UI reflects the change right away.
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:
enabled
: A boolean value that determines whether the query should be enabled. If set tofalse
, the query won’t run.retry
: The number of times the query should retry if it fails.retryDelay
: The delay in milliseconds between retries.refetchOnWindowFocus
: A boolean value that determines whether the query should refetch when the window regains focus.refetchOnMount
: A boolean value that determines whether the query should refetch when the component mounts.onSuccess
: A function that runs when the query is successful.onError
: A function that runs when the query fails.onSettled
: A function that runs when the query is either successful or failed.staleTime
: The time in milliseconds before the cache is considered stale.
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.
Posted on September 18, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.