Mastering Data Fetching with TanStack Query: A Modern Approach
By Vantol Bennett
Author
Introduction
Data fetching is one of the most common challenges developers face when building modern web applications. While it might seem straightforward at first, managing server state quickly becomes complex when you need to handle caching, background updates, pagination, and error states. This is where TanStack Query (formerly React Query) comes in—a powerful library that transforms how we think about server state management.
What is TanStack Query?
TanStack Query is a data-fetching and state management library that provides powerful tools for fetching, caching, synchronizing, and updating server state in your applications. Unlike traditional state management solutions, TanStack Query treats server data as a first-class citizen, recognizing that it has fundamentally different characteristics than client state.
The library works seamlessly with React, Vue, Svelte, and Solid, making it a versatile choice regardless of your framework preference. It eliminates the need for writing complex boilerplate code and helps you build more maintainable applications.
Why Use TanStack Query?#
Traditional data fetching approaches often require you to manage loading states, error handling, caching logic, and refetching strategies manually. TanStack Query handles all of this out of the box, providing:
- Automatic caching with intelligent cache invalidation
- Background refetching to keep data fresh
- Optimistic updates for better UX
- Request deduplication to prevent unnecessary network calls
- Pagination and infinite scroll support
- Prefetching for improved perceived performance
TanStack Query doesn't replace fetch or axios—it enhances them by providing a smart layer of abstraction for managing server state.
Getting Started
Let's dive into how you can start using TanStack Query in your React application.
Installation#
npm install @tanstack/react-query
# or
yarn add @tanstack/react-query
Basic Setup#
First, wrap your application with the QueryClientProvider:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
);
}
Your First Query#
Here's a simple example of fetching user data:
import { useQuery } from '@tanstack/react-query';
interface User {
id: number;
name: string;
email: string;
}
async function fetchUser(userId: string): Promise<User> {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
}
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
}
🔑 Query Keys
Query keys are crucial in TanStack Query. They uniquely identify your queries and are used for caching, refetching, and cache invalidation. Use arrays with descriptive values for better organization.
Advanced Features
Mutations#
Mutations handle data updates, creations, and deletions. Here's how to create a mutation for updating user data:
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface UpdateUserData {
name: string;
email: string;
}
function UpdateUserForm({ userId }: { userId: string }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (userData: UpdateUserData) =>
fetch(`/api/users/${userId}`, {
method: 'PUT',
body: JSON.stringify(userData),
headers: { 'Content-Type': 'application/json' },
}),
onSuccess: () => {
// Invalidate and refetch user data
queryClient.invalidateQueries({ queryKey: ['user', userId] });
},
});
const handleSubmit = (data: UpdateUserData) => {
mutation.mutate(data);
};
return (
<form onSubmit={(e) => {
e.preventDefault();
// Form handling logic
}}>
{mutation.isPending && <p>Updating...</p>}
{mutation.isError && <p>Error updating user</p>}
{mutation.isSuccess && <p>User updated successfully!</p>}
{/* Form fields */}
</form>
);
}
Optimistic Updates#
Optimistic updates provide instant feedback by updating the UI before the server responds:
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot the previous value
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update
queryClient.setQueryData(['todos'], (old: Todo[]) => [...old, newTodo]);
// Return context with the snapshot
return { previousTodos };
},
onError: (err, newTodo, context) => {
// Rollback on error
queryClient.setQueryData(['todos'], context?.previousTodos);
},
onSettled: () => {
// Refetch after error or success
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
Performance Benefits#
Optimistic updates can make your application feel up to 10x faster by eliminating the perceived latency of server responses.
Pagination#
TanStack Query makes pagination simple with built-in support:
function PaginatedPosts() {
const [page, setPage] = useState(1);
const { data, isLoading, isPlaceholderData } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
placeholderData: (previousData) => previousData,
});
return (
<div>
{data?.posts.map(post => <PostCard key={post.id} post={post} />)}
<button
onClick={() => setPage(old => Math.max(old - 1, 1))}
disabled={page === 1}
>
Previous
</button>
<button
onClick={() => setPage(old => old + 1)}
disabled={isPlaceholderData || !data?.hasMore}
>
Next
</button>
</div>
);
}
Best Practices
Always provide appropriate staleTime and gcTime values based on your data's characteristics. Not all data needs real-time updates!
Configure Cache Times Appropriately#
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 10, // 10 minutes (formerly cacheTime)
retry: 3,
refetchOnWindowFocus: true,
},
},
});
Use Query Keys Consistently#
Establish a naming convention for your query keys:
export const queryKeys = {
users: {
all: ['users'] as const,
detail: (id: string) => ['users', id] as const,
posts: (id: string) => ['users', id, 'posts'] as const,
},
posts: {
all: ['posts'] as const,
detail: (id: string) => ['posts', id] as const,
},
};
Enable DevTools#
TanStack Query comes with excellent developer tools:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
🛠️ DevTools Pro Tip
The DevTools panel provides real-time insights into your queries, their states, and cached data. It's invaluable for debugging and understanding how TanStack Query manages your data.
Common Patterns
Dependent Queries#
Sometimes you need to fetch data based on previous query results:
function UserPosts({ userId }: { userId: string }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchUserPosts(user!.id),
enabled: !!user, // Only run when user is available
});
return <PostsList posts={posts} />;
}
Prefetching#
Improve perceived performance by prefetching data:
function PostList() {
const queryClient = useQueryClient();
const { data: posts } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
const handleMouseEnter = (postId: string) => {
queryClient.prefetchQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
});
};
return (
<div>
{posts?.map(post => (
<div
key={post.id}
onMouseEnter={() => handleMouseEnter(post.id)}
>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</div>
))}
</div>
);
}
Conclusion
TanStack Query has fundamentally changed how developers approach data fetching and state management in modern applications. By providing intelligent caching, automatic background updates, and a simple yet powerful API, it eliminates much of the boilerplate code traditionally associated with server state management.
Whether you're building a small application or a large-scale enterprise system, TanStack Query's flexibility and performance optimizations make it an excellent choice. The library's active community, comprehensive documentation, and framework-agnostic approach ensure it will remain a valuable tool in your development arsenal.
Start small with basic queries and gradually adopt advanced features like optimistic updates and prefetching as your application grows. Your users will appreciate the snappier experience, and your team will benefit from cleaner, more maintainable code.
Happy querying!
Share this post
Stay Updated
Get the latest posts delivered straight to your inbox.
