Data Fetching with TanStack Query
Queries, mutations, pagination, infinite scroll, and optimistic updates
What You’ll Build
After following this guide, you will have a working implementation of data fetching with tanstack query in your project. TanStack Query (React Query) is the best way to fetch, cache, and synchronize server data in React. Eliminates loading/error state boilerplate, deduplicates requests, handles background refetching, and provides cache invalidation. No more useEffect + useState for API calls.
Use Cases & Problems Solved
- Implement interactive UI features that users expect from modern apps
- Follow established patterns that scale and remain maintainable
- Reduce boilerplate and avoid common frontend pitfalls
Prerequisites
- React 18+
Step-by-Step Implementation
Install and setup provider
The following snippet shows how to install and setup provider. Copy this into your project and adjust the values for your environment.
npm install @tanstack/react-query
Queries and mutations
The following snippet shows how to queries and mutations. Copy this into your project and adjust the values for your environment.
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Fetch data with automatic caching and refetching
function UserList() {
const { data: users, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
staleTime: 5 * 60 * 1000, // Consider fresh for 5 minutes
});
const queryClient = useQueryClient();
const createUser = useMutation({
mutationFn: (newUser) => fetch('/api/users', {
method: 'POST', body: JSON.stringify(newUser),
headers: { 'Content-Type': 'application/json' },
}).then(r => r.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] }); // Refetch list
},
});
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
{users.map(u => <p key={u.id}>{u.name}</p>)}
<button onClick={() => createUser.mutate({ name: 'New User' })}>
Add User
</button>
</div>
);
}
⚠️ Don’t Do This
❌ Using useEffect + useState for data fetching
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/users')
.then(r => r.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, []);
// No caching, no refetch, no deduplication, race conditions!
✅ Use TanStack Query — handles all edge cases automatically
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
});
// Cached, deduped, auto-refetches, handles race conditions!
Testing
Verify your implementation with these tests:
// __tests__/data-fetching-with-tanstack-query.test.ts
import { describe, it, expect } from 'vitest';
describe('Data Fetching with TanStack Query', () => {
it('should initialize without errors', () => {
// Test that the setup completes successfully
expect(() => setup()).not.toThrow();
});
it('should handle the primary use case', async () => {
const result = await execute();
expect(result).toBeDefined();
expect(result.success).toBe(true);
});
it('should handle edge cases', async () => {
// Test with empty/null input
const result = await execute(null);
expect(result.error).toBeDefined();
});
});
Verification
# Open React DevTools → install TanStack Query Devtools
# npm install @tanstack/react-query-devtools
# Open devtools panel → see all query cache entries
# Navigate away and back — data loads instantly from cache Related Specs
React Form Validation (React Hook Form + Zod)
Type-safe forms, custom validators, error display, and submission handling