Home › All rules › Community › Tanstack Query v5 Cursor Rules

Tanstack Query v5 Cursor Rules

Cursor rules for TanStack Query v5 with query options, query key factories, mutations, optimistic updates, infinite queries, Suspense, and prefetching.

CursorAny stackCommunity (CC0)

How to use

Save this as .cursor/rules/tanstack-query-v5-cursorrules-prompt-file.mdc in your project, commit it, and Cursor will load it. Review it first: community files can be opinionated or out of date.

Note: Review and test before use. Provided as is, without warranty. See Terms.

.cursor/rules/tanstack-query-v5-cursorrules-prompt-file.mdcDownload  
---
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

More: Any stack rules · all community rules

Related rules

Agent safety guardrails Windsurf rules

Always-on Windsurf rules that stop agents from running destructive, costly, or externally visible actions without approval.

WindsurfAny stack

Universal vibe-coding guardrails

Always-on Cursor rules that keep any AI agent careful: plan first, small diffs, verify, never fake it.

CursorAny stack

Prototype-to-production hardening AGENTS.md

AGENTS.md checklist for turning a vibe-coded prototype into a production-ready app: security, data, tests, and ops.

AGENTS.mdAny stack

Ascii Simulation Game Cursor Rules

Cursor rules for ASCII simulation game development.

CursorAny stackCommunity (CC0)

Clean Code Cursor Rules

Guidelines for writing clean, maintainable, and human-readable code. Apply these rules when writing or reviewing code to ensure consistency and quality.

CursorAny stackCommunity (CC0)

Code Guidelines Cursor Rules

Cursor rules for code development with guidelines integration.

CursorAny stackCommunity (CC0)

Get new rules weekly

One email a week: fresh rule files, agent tips, and what's changing in Cursor, Claude Code and Copilot. Free, unsubscribe anytime.

Opens the Rulestack Newsletter on beehiiv in a new tab. Or subscribe directly on beehiiv.