tanstack query v5
via PatrickJS/awesome-cursorrules
Expert guidance for TanStack Query v5: queries, mutations, optimistic updates, infinite queries, and Suspense patterns.
What is tanstack query v5?
TanStack Query v5 is a server state management library for React that handles data fetching, caching, and synchronization. Use this rule when building React applications that need robust async state management with query keys, mutations, prefetching, and cache invalidation strategies.
- Define reusable queries with queryOptions() helper and structured query key factories
- Implement mutations with optimistic updates and cache invalidation patterns
- Build infinite queries with cursor-based pagination and lazy loading
- Prefetch data on hover or during routing to eliminate loading states
- Use Suspense mode with useSuspenseQuery for cleaner async component patterns
- Configure stale time, retry logic, and error handling for production reliability
Applies to
File patterns this rule matches.
Rule definition (reference)
Source of truth, from the repository.
You are an expert in TanStack Query v5 (formerly React Query), TypeScript, and async state management for React applications.
TanStack Query v5 Guidelines
Core Philosophy
- TanStack Query manages server state — it is NOT a general state manager for client-only state
- Every query should have a stable, serializable query key that uniquely describes the data
- Mutations handle writes; queries handle reads — never blur this boundary
- Prefer
queryOptions()helper for reusable, co-located query definitions - v5 breaking changes:
useQueryno longer accepts positional args; always use the options object form
Setup
// main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute default stale time
retry: 2,
refetchOnWindowFocus: true,
},
},
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
Query Keys
- Always structure keys as arrays:
['entity', 'list'],['entity', 'detail', id] - Use a query key factory to avoid typos and enable easy invalidation
// queryKeys.ts
export const postKeys = {
all: ['posts'] as const,
lists: () => [...postKeys.all, 'list'] as const,
list: (filters: PostFilters) => [...postKeys.lists(), filters] as const,
details: () => [...postKeys.all, 'detail'] as const,
detail: (id: string) => [...postKeys.details(), id] as const,
}
queryOptions Helper (v5)
- Use
queryOptions()to define queries once and reuse across components and loaders
import { queryOptions } from '@tanstack/react-query'
export const postQueryOptions = (id: string) =>
queryOptions({
queryKey: postKeys.detail(id),
queryFn: () => fetchPost(id),
staleTime: 1000 * 60 * 5, // 5 min
})
// In component
const { data } = useQuery(postQueryOptions(postId))
// In router loader (TanStack Router integration)
loader: ({ params, context: { queryClient } }) =>
queryClient.ensureQueryData(postQueryOptions(params.postId))
useQuery
const {
data,
isLoading, // true only on first load with no cached data
isFetching, // true whenever a fetch is in-flight
isError,
error,
isSuccess,
} = useQuery({
queryKey: postKeys.detail(postId),
queryFn: () => fetchPost(postId),
enabled: !!postId, // disable query if params not ready
})
useMutation
const { mutate, mutateAsync, isPending } = useMutation({
mutationFn: (newPost: CreatePostInput) => createPost(newPost),
onSuccess: (data) => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: postKeys.lists() })
toast.success('Post created!')
},
onError: (error) => {
toast.error(error.message)
},
})
// Usage
mutate({ title: 'Hello', body: '...' })
Optimistic Updates
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: updatePost,
onMutate: async (updatedPost) => {
await queryClient.cancelQueries({ queryKey: postKeys.detail(updatedPost.id) })
const previous = queryClient.getQueryData(postKeys.detail(updatedPost.id))
queryClient.setQueryData(postKeys.detail(updatedPost.id), updatedPost)
return { previous }
},
onError: (err, updatedPost, context) => {
queryClient.setQueryData(postKeys.detail(updatedPost.id), context?.previous)
},
onSettled: (_, __, updatedPost) => {
queryClient.invalidateQueries({ queryKey: postKeys.detail(updatedPost.id) })
},
})
Infinite Queries
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: postKeys.lists(),
queryFn: ({ pageParam }) => fetchPosts({ cursor: pageParam }),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
// data.pages is an array of page results — flatten for rendering
const allPosts = data?.pages.flatMap((page) => page.items) ?? []
Prefetching
- Prefetch on hover or during routing to eliminate loading states
// Hover prefetch
const handleMouseEnter = () => {
queryClient.prefetchQuery(postQueryOptions(postId))
}
// In router loader (eliminates all loading spinners)
export const Route = createFileRoute('/posts/$postId')({
loader: ({ context: { queryClient }, params }) =>
queryClient.ensureQueryData(postQueryOptions(params.postId)),
})
Cache Invalidation Patterns
// Invalidate all post queries
queryClient.invalidateQueries({ queryKey: postKeys.all })
// Invalidate only post lists
queryClient.invalidateQueries({ queryKey: postKeys.lists() })
// Remove from cache entirely
queryClient.removeQueries({ queryKey: postKeys.detail(id) })
// Directly update cache without refetch
queryClient.setQueryData(postKeys.detail(id), newData)
Suspense Mode
- Use
useSuspenseQueryfor Suspense-based data fetching (v5) - Wrap with
<Suspense fallback={<Skeleton />}> - Pair with
<ErrorBoundary>for error handling
// No need to handle isLoading — Suspense handles it
const { data } = useSuspenseQuery(postQueryOptions(postId))
Performance Best Practices
- Set appropriate
staleTimeper query — defaults to0(always stale) - Use
selectto transform/subscribe to only relevant slices of data - Use
placeholderData: keepPreviousDatafor pagination to avoid layout shifts - Avoid creating
QueryClientinside components — instantiate once at app root - Use
notifyOnChangePropsto limit re-renders to only relevant data changes
Error Handling
- Use
throwOnError: trueto bubble errors to the nearest ErrorBoundary - Use
retryfunction for conditional retry logic (e.g., skip retry on 404)
retry: (failureCount, error) => {
if (error.status === 404) return false
return failureCount < 3
},
TypeScript Tips
- Always type
queryFnreturn value explicitly or infer from typed API functions - Use
QueryObserverResult<TData, TError>to type hook return values - Use
UseMutationResult<TData, TError, TVariables>for mutations
Related rules
Senior full-stack TypeScript, React, Node.js guidance with clean architecture, testing, and WHY-oriented reasoning.
Quantitative factor research skills for designing, evaluating, and mining alpha factors in equities markets.
Android development with Jetpack Compose, clean architecture, and Material Design 3.
Angular development with Novo Elements UI library using standalone components.
Expert Angular 18 + TypeScript development with Jest, emphasizing clean code and performance.
Manage Kubernetes clusters, add-ons, stacks, and credentials via the Ankra CLI platform.