Integrating REST APIs Seamlessly with React Query
Introduction
Fetching data from REST APIs is a fundamental part of any modern web application. However, managing states like loading, error handling, and caching can quickly become complex. React Query provides an elegant abstraction for dealing with asynchronous data fetching, simplifying state synchronization and cache management out of the box. In this article, we’ll build a complete example to show how React Query helps integrate REST APIs efficiently with React components.
Section 1: Understanding React Query Basics
React Query, built by Tanner Linsley, offers hooks for fetching, caching, and synchronizing server-side data. Its primary goal is to minimize the boilerplate typically required for handling remote data. Let’s install it in a React project:
npm install @tanstack/react-query axios
Once installed, wrap your application with a QueryClientProvider to initialize React Query’s client instance:
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
const queryClient = new QueryClient();
const Root = () => (
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);
export default Root;
This setup ensures that all React components under QueryClientProvider can use hooks like useQuery and useMutation. It also enables a centralized caching behavior across the app.
Section 2: Fetching Data from a REST API
Let’s consume a REST endpoint that lists users using axios for the HTTP calls and useQuery for data management. Here’s a full working example:
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
const fetchUsers = async () => {
const { data } = await axios.get('https://jsonplaceholder.typicode.com/users');
return data;
};
const UserList = () => {
const { data, isLoading, error } = useQuery(['users'], fetchUsers);
if (isLoading) return <p>Loading users...</p>;
if (error) return <p>Error loading users: {error.message}</p>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
export default UserList;
React Query automatically caches this data using the key ['users']. When the same key is queried again, React Query pulls data from the cache unless it has expired or been invalidated.
Section 3: Automatic Refetching and Stale-While-Revalidate Strategy
React Query’s caching layer uses a concept called “stale-time” that defines how long data is considered fresh. For example, setting staleTime: 60000 means data won’t be refetched for 60 seconds. This improves performance and reduces unnecessary network calls.
const { data } = useQuery(['users'], fetchUsers, {
staleTime: 60000, // cached data considered fresh for 1 minute
refetchOnWindowFocus: true, // refetch when window regains focus
});
React Query will automatically revalidate the data when the user returns to the tab or triggers a refetch manually. This approach ensures users see up-to-date data without you having to manually control state transitions or timers.
Section 4: Mutations – Creating or Updating Data
Besides fetching, React Query handles data mutations seamlessly. For example, adding a new user can be implemented using useMutation combined with an API POST request. You can also invalidate cached queries to refresh your UI after the mutation.
import { useMutation, useQueryClient } from '@tanstack/react-query';
const addUser = async (newUser) => {
return axios.post('https://jsonplaceholder.typicode.com/users', newUser);
};
const AddUserForm = () => {
const queryClient = useQueryClient();
const mutation = useMutation(addUser, {
onSuccess: () => queryClient.invalidateQueries(['users']),
});
const handleSubmit = (e) => {
e.preventDefault();
const form = e.target;
mutation.mutate({ name: form.username.value });
form.reset();
};
return (
<form onSubmit={handleSubmit}>
<input name="username" placeholder="New user name" />
<button disabled={mutation.isLoading}>Add</button>
</form>
);
};
After successfully adding a new user, invalidateQueries ensures our UserList component fetches updated data automatically. This reactive model keeps client data synchronized with the server in a declarative manner.
Section 5: Optimizing Performance and DevTools Insights
React Query offers several options to optimize data requests. You can enable pagination, background synchronization, or even offline caching with persistent storage. The DevTools integration provides visibility into query states, retries, and revalidation patterns.
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
The DevTools allow you to inspect cached data, refetch intervals, and metadata about background fetching activities. This helps developers debug and enhance performance strategies effectively.
Conclusion
React Query simplifies consuming REST APIs in a React app by managing asynchronous states, caching, and background synchronization automatically. With minimal code, developers gain robust caching and revalidation mechanisms that traditionally required extensive manual boilerplate. For modern front-end applications, adopting React Query is a substantial productivity boost with long-term maintainability benefits.
Useful links:


