For years, I relied on the classic useEffect and useState pattern to fetch data in React. It worked, but as my apps grew, I found myself writing the same boilerplate over and over: isLoading, isError, and complex cleanup functions to avoid memory leaks. Then I discovered TanStack Query.
If you’re looking for a definitive tanstack query react tutorial, you’re in the right place. TanStack Query (formerly React Query) isn’t just a data-fetching library; it’s a server-state management tool. It handles caching, deduplication, and background updates automatically, allowing you to focus on the UI rather than the plumbing of your API calls.
Want to scale this further? If you’re building a full-scale application, check out my guide on how to build a SaaS dashboard with Next.js to see these patterns in a production environment.
Prerequisites
Before we dive into the code, make sure you have the following set up:
- A basic understanding of React hooks (specifically
useStateanduseEffect). - A running React project (Vite is my personal recommendation for speed).
- Node.js installed on your machine.
- A public API to test with (we’ll use JSONPlaceholder for this tutorial).
Step 1: Installation and Setup
First, we need to install the core package. I always recommend installing the DevTools as well—they are an absolute lifesaver for visualizing how your cache behaves.
npm install @tanstack/react-query
npm install @tanstack/react-query-devtools
To use TanStack Query, you must wrap your application in a QueryClientProvider. This provides the cache context to all your components. In my experience, keeping this in your main.tsx or App.tsx is the cleanest approach.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourAppComponents />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
Step 2: Fetching Data with useQuery
The heart of any TanStack Query React tutorial is the useQuery hook. It takes a unique key and a function that returns a promise. The key is crucial because TanStack Query uses it to cache the data; if two components call the same key, they share the same data.
import { useQuery } from '@tanstack/react-query';
const fetchPosts = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
};
function PostsList() {
const { data, isLoading, isError, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
if (isLoading) return <p>Loading posts...</p>;
if (isError) return <p>Error: {error.message}</p>;
return (
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul> collett
);
}
As shown in the DevTools (which you should have installed in Step 1), you’ll notice that when you navigate away and back to this component, the data appears instantly. This is because it’s being served from the cache while a background refetch triggers to ensure the data is fresh.
Step 3: Updating Data with useMutation
While useQuery is for GET requests, useMutation is for creating, updating, or deleting data. The most important part of a mutation is cache invalidation. After you update a post, you want TanStack Query to know that the ‘posts’ list is now outdated.
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreatePost() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newPost) => {
return fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: JSON.stringify(newPost),
});
},
onSuccess: () => {
// This tells TanStack Query to refetch the 'posts' query
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
return (
<button onClick={() => mutation.mutate({ title: 'New Post' })}>
Add Post
</button>
);
}
I’ve found that failing to invalidate queries is the #1 reason developers think their app has a bug when it’s actually just a caching issue. Always think: “Which queries are affected by this change?”
Pro Tips for Production
- Stale Time: By default, data is considered stale immediately. Use
staleTime: 1000 * 60 * 5(5 minutes) to stop excessive API calls for data that doesn’t change often. - Error Boundaries: Instead of handling
isErrorin every component, integrate TanStack Query with React Error Boundaries for a cleaner global error state. - Prefetching: You can prefetch data when a user hovers over a link using
queryClient.prefetchQueryto make the navigation feel instantaneous.
If you are moving toward a more modern architecture, you might be curious about react server components best practices. While TanStack Query is king for client-side state, RSCs can handle the initial fetch on the server, reducing the amount of JS sent to the browser.
Troubleshooting Common Issues
“My data isn’t updating!”
Check your queryKey. If you are fetching a specific item, your key should be ['posts', postId], not just ['posts']. Unique keys are the only way the cache knows what to invalidate.
“I’m getting too many network requests.”
This is usually due to the default refetchOnWindowFocus setting. If your data isn’t real-time, set this to false in your QueryClient configuration.
What’s Next?
Now that you’ve mastered the basics of this tanstack query react tutorial, I recommend exploring Infinite Queries for pagination and Optimistic Updates to make your UI feel lightning-fast by updating the cache before the server even responds.
Looking for more frontend optimization? Subscribe to my newsletter for weekly deep-dives into automation and dev tools.