tanstack-query-v5-cursorrules-prompt-file — Cursor rule: TanStack
A Cursor rule that tells the agent how to write TanStack Query v5 code: query keys, mutations, optimistic updates, Suspense and prefetching.
.cursor/rules/tanstack-query-v5-cursorrules-prompt-file.mdcWhat tanstack-query-v5-cursorrules-prompt-file does
This is a Cursor rule file you drop into a project to steer the agent's TanStack Query v5 code. It sets conventions: query keys as arrays, a key factory instead of loose strings, a clean split between reads in useQuery and writes in useMutation, and the queryOptions() helper for definitions you reuse.
It also carries working snippets: QueryClient setup in main.tsx, optimistic updates with rollback in onError, infinite queries via getNextPageParam, prefetching on hover and in router loaders, cache invalidation patterns. One v5 breaking change gets called out — useQuery no longer takes positional arguments, only the options object. If your codebase still has the old call style, this rule will push it toward the new syntax, so check that before wiring it in.
Separate sections cover Suspense with useSuspenseQuery, error handling through throwOnError and a conditional retry, plus TypeScript notes. The rule does not apply on its own: frontmatter sets alwaysApply: false, and globs is **/*.
What it helps with
- unify query keys behind a single factory
- migrate useQuery calls to the v5 options object
- add an optimistic update that rolls back on failure
- wire cursor-based infinite scrolling for a list
- kill loading spinners by prefetching in a loader
Example prompts for the agent
- Create a query key factory for the post entity following the TanStack Query v5 rules
- Add a create-post mutation that invalidates the list and shows a success toast
- Convert this component to useSuspenseQuery with Suspense and an ErrorBoundary
How to install tanstack-query-v5-cursorrules-prompt-file
Manually: put the files in place
.cursor/rules/tanstack-query-v5-cursorrules-prompt-file.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/tanstack-query-v5-cursorrules-prompt-file.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/tanstack-query-v5-cursorrules-prompt-file.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/tanstack-query-v5-cursorrules-prompt-file.mdManually: put the files in place
.clinerules/tanstack-query-v5-cursorrules-prompt-file.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about tanstack-query-v5-cursorrules-prompt-file
How do I install tanstack-query-v5-cursorrules-prompt-file in Cursor?
Create .cursor/rules/tanstack-query-v5-cursorrules-prompt-file.mdc in your project and paste the rule contents into it.
What does this rule do?
It gives the Cursor agent TanStack Query v5 conventions: key structure, queryOptions(), mutations, optimistic updates, infinite queries, Suspense and prefetching.
Does the rule apply to every file automatically?
No. Frontmatter sets alwaysApply: false, so you enable it deliberately, even though globs covers **/*.
Source text
tanstack-query-v5-cursorrules-prompt-file.mdc · CC0-1.0
---
description: "Cursor rules for TanStack Query v5 with query options, query key factories, mutations, optimistic updates, infinite queries, Suspense, and prefetching."
globs: **/*
alwaysApply: false
---
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: `useQuery` no longer accepts positional args; always use the options object form
## Setup
```tsx
// 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
```ts
// 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
```ts
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```ts
// 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
```ts
// 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 `useSuspenseQuery` for Suspense-based data fetching (v5)
- Wrap with `<Suspense fallback={<Skeleton />}>`
- Pair with `<ErrorBoundary>` for error handling
```tsx
// No need to handle isLoading — Suspense handles it
const { data } = useSuspenseQuery(postQueryOptions(postId))
```
## Performance Best Practices
- Set appropriate `staleTime` per query — defaults to `0` (always stale)
- Use `select` to transform/subscribe to only relevant slices of data
- Use `placeholderData: keepPreviousData` for pagination to avoid layout shifts
- Avoid creating `QueryClient` inside components — instantiate once at app root
- Use `notifyOnChangeProps` to limit re-renders to only relevant data changes
## Error Handling
- Use `throwOnError: true` to bubble errors to the nearest ErrorBoundary
- Use `retry` function for conditional retry logic (e.g., skip retry on 404)
```ts
retry: (failureCount, error) => {
if (error.status === 404) return false
return failureCount < 3
},
```
## TypeScript Tips
- Always type `queryFn` return value explicitly or infer from typed API functions
- Use `QueryObserverResult<TData, TError>` to type hook return values
- Use `UseMutationResult<TData, TError, TVariables>` for mutations
© PatrickJS · CC0-1.0 · PatrickJS/awesome-cursorrules