tanstack-query — Cursor rule: what it does, how to install
This Cursor rule guides TanStack Query v5 usage in React and TypeScript. Use it when building queries, mutations, pagination, or Suspense flows.
.cursor/rules/tanstack-query.mdcWhat tanstack-query does
The rule guides Cursor through TanStack Query v5 patterns for server state. It covers stable query keys, key factories, reusable queryOptions() definitions, mutations with cache invalidation, optimistic changes, and infinite queries.
It also includes guidance for useSuspenseQuery, prefetching, and pagination with placeholderData: keepPreviousData. The rule targets src/**/*.tsx, src/**/*.ts, and src/queries/**/*; alwaysApply is set to false.
In v5, useQuery takes an options object, so positional arguments do not fit this rule. It also keeps QueryClient at the application root rather than creating one inside a component.
What it helps with
- organize query keys with a factory
- define reusable queries with
queryOptions() - invalidate cached data after a mutation
- roll back an optimistic cache change
- build cursor-based infinite queries
Example prompts for the agent
- Set up the posts data layer with TanStack Query v5 and a query key factory.
- Rewrite this
useQuerycall with the v5 options-object syntax andqueryOptions(). - Add an optimistic post update that restores the previous cache value when the mutation fails.
How to install tanstack-query
Manually: put the files in place
.cursor/rules/tanstack-query.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/tanstack-query.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.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.mdManually: put the files in place
.clinerules/tanstack-query.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about tanstack-query
How do I install tanstack-query in Cursor?
Create the rule file at .cursor/rules/tanstack-query.mdc.
What does the tanstack-query rule do?
It gives Cursor TanStack Query v5 patterns for queries, mutations, cache updates, optimistic changes, infinite queries, and Suspense.
Which files does tanstack-query apply to?
It targets src/**/*.tsx, src/**/*.ts, and src/queries/**/*. The rule is not always active because alwaysApply is set to false.
Source text
tanstack-query.mdc · CC0-1.0
---
description: "TanStack Query v5 (React Query) patterns including queryOptions helper, query key factories, mutations, optimistic updates, infinite queries, Suspense mode, and prefetching"
globs: ["src/**/*.tsx", "src/**/*.ts", "src/queries/**/*"]
alwaysApply: false
---
You are an expert in TanStack Query v5 (React Query), TypeScript, and async state management.
## Core Principles
- TanStack Query manages server state — NOT a general client state manager
- Every query needs a stable, serializable query key that uniquely describes the data
- Mutations handle writes; queries handle reads — never blur this boundary
- Use `queryOptions()` helper (v5) for reusable, co-located query definitions
- v5 breaking change: `useQuery` only accepts options object form — no positional args
## QueryClient Setup
```tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60,
retry: (count, error: any) => error?.status !== 404 && count < 2,
},
},
})
```
## Query Key Factory Pattern
```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)
```ts
export const postQueryOptions = (id: string) =>
queryOptions({
queryKey: postKeys.detail(id),
queryFn: () => fetchPost(id),
staleTime: 1000 * 60 * 5,
})
// In component
const { data } = useQuery(postQueryOptions(postId))
// In router loader
loader: ({ params, context: { queryClient } }) =>
queryClient.ensureQueryData(postQueryOptions(params.postId))
```
## Mutations
```tsx
const { mutate, isPending } = useMutation({
mutationFn: (input: CreatePostInput) => createPost(input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: postKeys.lists() })
},
onError: (error) => toast.error(error.message),
})
```
## Optimistic Updates
```tsx
const mutation = useMutation({
mutationFn: updatePost,
onMutate: async (updated) => {
await queryClient.cancelQueries({ queryKey: postKeys.detail(updated.id) })
const previous = queryClient.getQueryData(postKeys.detail(updated.id))
queryClient.setQueryData(postKeys.detail(updated.id), updated)
return { previous }
},
onError: (_, updated, ctx) => {
queryClient.setQueryData(postKeys.detail(updated.id), ctx?.previous)
},
onSettled: (_, __, updated) => {
queryClient.invalidateQueries({ queryKey: postKeys.detail(updated.id) })
},
})
```
## Infinite Queries
```tsx
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: postKeys.lists(),
queryFn: ({ pageParam }) => fetchPosts({ cursor: pageParam }),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
const allPosts = data?.pages.flatMap((p) => p.items) ?? []
```
## Suspense Mode (v5)
```tsx
// useSuspenseQuery — no isLoading needed, Suspense handles it
const { data } = useSuspenseQuery(postQueryOptions(postId))
// Wrap with <Suspense fallback={<Skeleton />}> + <ErrorBoundary>
```
## Key Rules
- Always define `queryOptions` outside components — never inline in `useQuery()`
- Never use `useEffect` to fetch data — use loaders or `useQuery`
- Use `placeholderData: keepPreviousData` for pagination to avoid layout shifts
- Instantiate `QueryClient` once at app root — never inside a component
© PatrickJS · CC0-1.0 · PatrickJS/awesome-cursorrules