| name | tanstack-query |
| description | TanStack Query v5 patterns and common pitfalls. Use when implementing data fetching, cache invalidation, or debugging stale data issues. Triggers on useQuery, useMutation, queryKey, invalidate, TanStack, React Query, data fetching, server state. |
TanStack Query v5 Patterns
Common pitfalls and correct patterns for TanStack Query v5 in Next.js applications.
When to Use
- Implementing data fetching with useQuery
- Setting up mutations with useMutation
- Debugging stale data or cache issues
- Reviewing code that uses TanStack Query
- Building infinite scroll or pagination
Workflow
Step 1: Check Query Keys
Verify query keys use full URL paths for proper deduplication.
Step 2: Verify Invalidation
Ensure mutations invalidate relevant queries on success.
Step 3: Check v5 Patterns
Verify v5-specific patterns (isPending vs isLoading).
Correct Usage
const { data } = useQuery({
queryKey: ['/api/v1/resources', resourceId],
queryFn: () => api.get(`/api/v1/resources/${resourceId}`),
});
const mutation = useMutation({
mutationFn: (data) => api.post('/api/v1/resources', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/v1/resources'] });
},
});
import type { Resource } from '@/lib/api';
const { data } = useQuery<{ data: Resource[] }>({
queryKey: ['/api/v1/resources'],
queryFn: fetchResources,
});
Anti-Patterns
queryKey: ['resources']
onSuccess: () => { router.push('/'); }
mutation.isLoading
Vibe4Vets Patterns
Resource List with Filters
import { useQuery } from '@tanstack/react-query';
import { api } from '@/lib/api';
interface ResourceFilters {
category?: string;
state?: string;
limit?: number;
offset?: number;
}
export function useResources(filters: ResourceFilters = {}) {
return useQuery({
queryKey: ['/api/v1/resources', filters],
queryFn: async () => {
const params = new URLSearchParams();
if (filters.category) params.set('category', filters.category);
if (filters.state) params.set('state', filters.state);
if (filters.limit) params.set('limit', String(filters.limit));
if (filters.offset) params.set('offset', String(filters.offset));
const response = await api.get(`/api/v1/resources?${params}`);
return response.data;
},
staleTime: 5 * 60 * 1000,
});
}
Infinite Scroll for Discovery Feed
import { useInfiniteQuery } from '@tanstack/react-query';
import { api } from '@/lib/api';
export function useResourcesInfinite(filters: ResourceFilters = {}) {
return useInfiniteQuery({
queryKey: ['/api/v1/resources', 'infinite', filters],
queryFn: async ({ pageParam = 0 }) => {
const params = new URLSearchParams();
params.set('offset', String(pageParam));
params.set('limit', '20');
if (filters.category) params.set('category', filters.category);
const response = await api.get(`/api/v1/resources?${params}`);
return response.data;
},
getNextPageParam: (lastPage, allPages) => {
if (lastPage.length < 20) return undefined;
return allPages.flat().length;
},
initialPageParam: 0,
});
}
Search with Debounce
import { useQuery } from '@tanstack/react-query';
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
export function useSearch(query: string) {
const debouncedQuery = useDebouncedValue(query, 300);
return useQuery({
queryKey: ['/api/v1/search', debouncedQuery],
queryFn: async () => {
if (!debouncedQuery || debouncedQuery.length < 2) return [];
const response = await api.get(`/api/v1/search?q=${encodeURIComponent(debouncedQuery)}`);
return response.data;
},
enabled: debouncedQuery.length >= 2,
staleTime: 10 * 60 * 1000,
});
}
Admin Mutations
import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useReviewResource() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ resourceId, approved }: { resourceId: number; approved: boolean }) => {
const response = await api.post(`/api/v1/admin/review/${resourceId}`, { approved });
return response.data;
},
onSuccess: (_, { resourceId }) => {
queryClient.invalidateQueries({ queryKey: ['/api/v1/resources', resourceId] });
queryClient.invalidateQueries({ queryKey: ['/api/v1/admin/review-queue'] });
},
onError: (error) => {
toast.error('Failed to review resource');
},
});
}
Optimistic Updates
const mutation = useMutation({
mutationFn: updateResource,
onMutate: async (newData) => {
await queryClient.cancelQueries({ queryKey: ['/api/v1/resources', id] });
const previous = queryClient.getQueryData(['/api/v1/resources', id]);
queryClient.setQueryData(['/api/v1/resources', id], newData);
return { previous };
},
onError: (err, newData, context) => {
queryClient.setQueryData(['/api/v1/resources', id], context?.previous);
toast.error('Update failed');
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['/api/v1/resources', id] });
},
});
Provider Setup
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
refetchOnWindowFocus: false,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
Quick Checklist