Mastering React Query for Server State
Introduction to React Query
Managing Server State
Most React applications need to talk to a server. They fetch data to display, send updates back to the server, and try to keep what the user sees in sync with the data stored in a database somewhere far away. This is often called “server state.”
Managing this state can get complicated. You need to handle loading states, error messages, and caching data to avoid unnecessary network requests. While you can do all this with React's built-in hooks like useState and useEffect, it often leads to a lot of repetitive, complex, and error-prone code.
React Query is a library that simplifies fetching, caching, and updating data from a server. It handles the tricky parts so you can focus on building your app.
Think of React Query as a smart assistant for your app's data. It fetches data when you need it, keeps a cached copy handy to make your app feel faster, and automatically refetches data in the background to make sure it's not stale. Let's look at its core concepts.
Queries for Fetching Data
A query is a request for data. In React Query, you use the useQuery hook to fetch data from an API. You give it a unique key for that data and a function that fetches it. React Query takes care of the rest.
Here’s what useQuery handles for you:
- Loading state: It tells you when the data is being fetched.
- Error state: It catches any errors that happen during the fetch.
- Caching: It automatically saves the fetched data. If you ask for the same data again, it gives you the cached version first while it re-fetches in the background to see if anything has changed.
import { useQuery } from '@tanstack/react-query';
function Todos() {
const { isLoading, error, data } = useQuery({
queryKey: ['todos'],
queryFn: () =>
fetch('https://api.example.com/todos').then((res) =>
res.json(),
),
});
if (isLoading) return 'Loading...';
if (error) return 'An error has occurred: ' + error.message;
// We have data!
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
TanStack Query brings much-needed predictability, efficiency, and professional structure to the often-messy world of server-state management in React.
With useQuery, you don't need to manually set loading states or store data in component state. The hook provides everything you need in a simple, declarative way.
Mutations for Changing Data
While queries are for reading data, mutations are for creating, updating, or deleting it. Anything that changes data on the server is a mutation.
React Query gives us the useMutation hook for this. You provide it a function that performs the change (like making a POST, PUT, or DELETE request), and it gives you a mutate function to call when you're ready to make the change.
import { useMutation } from '@tanstack/react-query';
function AddTodo() {
const mutation = useMutation({
mutationFn: (newTodo) => {
return fetch('https://api.example.com/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
});
}
});
return (
<button
onClick={() => {
mutation.mutate({ id: new Date(), title: 'Do Laundry' });
}}
>
Add Todo
</button>
);
}
The useMutation hook also provides states like isLoading and isError, just like useQuery, so you can easily show feedback to the user while the mutation is in progress.
Keeping Data Fresh
So, you've used a mutation to add a new to-do item. How does your list of to-dos update to show the new one? This is where query invalidation comes in.
After a mutation succeeds, you can tell React Query that certain data is now out-of-date, or “stale.” When you invalidate a query, React Query will automatically refetch it to get the latest version. You do this using the queryKey you defined earlier.
This pattern is incredibly powerful. Your components that create or update data don't need to know about the components that display the data. They just need to invalidate the right query key, and React Query ensures that everything on the screen updates automatically.
This separation makes your code cleaner and much easier to manage as your application grows. It eliminates the need to manually pass new data around or trigger state updates in different parts of your app.
What is the primary problem React Query is designed to solve?
After a user successfully adds a new comment via a form, you want the list of comments on the page to automatically update. What is the standard React Query pattern to achieve this?
By handling the complexities of server state, React Query lets you write cleaner, more declarative, and more resilient React applications.