PluginBench
Rule

tanstack query

via PatrickJS/awesome-cursorrules

TanStack Query v5 patterns: queryOptions, query key factories, mutations, optimistic updates, and Suspense mode.

What is tanstack query?

Implements TanStack Query v5 best practices for server state management in React. Use this rule when building data-fetching layers with TypeScript, especially for applications requiring mutations, infinite queries, prefetching, or Suspense integration. Enforces stable query keys, co-located query definitions, and proper separation of reads and writes.

  • Define reusable queries with queryOptions() helper and query key factory patterns
  • Handle mutations with automatic cache invalidation and error handling
  • Implement optimistic updates with rollback on failure
  • Build infinite queries with cursor-based pagination
  • Enable Suspense mode with useSuspenseQuery for cleaner async boundaries
  • Prefetch data in loaders using ensureQueryData() for SSR and router integration

Applies to

File patterns this rule matches.

["src/**/*.tsx"
src/**/*.ts
"src/queries/**/*"]
Rule definition (reference)

Source of truth, from the repository.

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60,
      retry: (count, error: any) => error?.status !== 404 && count < 2,
    },
  },
})

Query Key Factory Pattern

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)

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

const { mutate, isPending } = useMutation({
  mutationFn: (input: CreatePostInput) => createPost(input),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: postKeys.lists() })
  },
  onError: (error) => toast.error(error.message),
})

Optimistic Updates

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

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)

// 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

Related rules

Senior full-stack TypeScript, React, Node.js guidance with clean architecture, testing, and WHY-oriented reasoning.

**/*
41k
via PatrickJS/awesome-cursorrules

Quantitative factor research skills for designing, evaluating, and mining alpha factors in equities markets.

**/*
41k
via PatrickJS/awesome-cursorrules

Android development with Jetpack Compose, clean architecture, and Material Design 3.

**/*
41k
via PatrickJS/awesome-cursorrules

Angular development with Novo Elements UI library using standalone components.

**/*
41k
via PatrickJS/awesome-cursorrules

Expert Angular 18 + TypeScript development with Jest, emphasizing clean code and performance.

**/*
41k
via PatrickJS/awesome-cursorrules

Manage Kubernetes clusters, add-ons, stacks, and credentials via the Ankra CLI platform.

**/*.sh +5
41k
via PatrickJS/awesome-cursorrules