{"filename":"tanstack-query-v5-cursorrules-prompt-file.mdc","content":"---\ndescription: \"Cursor rules for TanStack Query v5 with query options, query key factories, mutations, optimistic updates, infinite queries, Suspense, and prefetching.\"\nglobs: **/*\nalwaysApply: false\n---\nYou are an expert in TanStack Query v5 (formerly React Query), TypeScript, and async state management for React applications.\n\n# TanStack Query v5 Guidelines\n\n## Core Philosophy\n- TanStack Query manages server state — it is NOT a general state manager for client-only state\n- Every query should have a stable, serializable query key that uniquely describes the data\n- Mutations handle writes; queries handle reads — never blur this boundary\n- Prefer `queryOptions()` helper for reusable, co-located query definitions\n- v5 breaking changes: `useQuery` no longer accepts positional args; always use the options object form\n\n## Setup\n```tsx\n// main.tsx\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { ReactQueryDevtools } from '@tanstack/react-query-devtools'\n\nconst queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      staleTime: 1000 * 60, // 1 minute default stale time\n      retry: 2,\n      refetchOnWindowFocus: true,\n    },\n  },\n})\n\nfunction App() {\n  return (\n    <QueryClientProvider client={queryClient}>\n      <YourApp />\n      <ReactQueryDevtools initialIsOpen={false} />\n    </QueryClientProvider>\n  )\n}\n```\n\n## Query Keys\n- Always structure keys as arrays: `['entity', 'list']`, `['entity', 'detail', id]`\n- Use a query key factory to avoid typos and enable easy invalidation\n```ts\n// queryKeys.ts\nexport const postKeys = {\n  all: ['posts'] as const,\n  lists: () => [...postKeys.all, 'list'] as const,\n  list: (filters: PostFilters) => [...postKeys.lists(), filters] as const,\n  details: () => [...postKeys.all, 'detail'] as const,\n  detail: (id: string) => [...postKeys.details(), id] as const,\n}\n```\n\n## queryOptions Helper (v5)\n- Use `queryOptions()` to define queries once and reuse across components and loaders\n```ts\nimport { queryOptions } from '@tanstack/react-query'\n\nexport const postQueryOptions = (id: string) =>\n  queryOptions({\n    queryKey: postKeys.detail(id),\n    queryFn: () => fetchPost(id),\n    staleTime: 1000 * 60 * 5, // 5 min\n  })\n\n// In component\nconst { data } = useQuery(postQueryOptions(postId))\n\n// In router loader (TanStack Router integration)\nloader: ({ params, context: { queryClient } }) =>\n  queryClient.ensureQueryData(postQueryOptions(params.postId))\n```\n\n## useQuery\n```tsx\nconst {\n  data,\n  isLoading,    // true only on first load with no cached data\n  isFetching,   // true whenever a fetch is in-flight\n  isError,\n  error,\n  isSuccess,\n} = useQuery({\n  queryKey: postKeys.detail(postId),\n  queryFn: () => fetchPost(postId),\n  enabled: !!postId, // disable query if params not ready\n})\n```\n\n## useMutation\n```tsx\nconst { mutate, mutateAsync, isPending } = useMutation({\n  mutationFn: (newPost: CreatePostInput) => createPost(newPost),\n  onSuccess: (data) => {\n    // Invalidate and refetch\n    queryClient.invalidateQueries({ queryKey: postKeys.lists() })\n    toast.success('Post created!')\n  },\n  onError: (error) => {\n    toast.error(error.message)\n  },\n})\n\n// Usage\nmutate({ title: 'Hello', body: '...' })\n```\n\n## Optimistic Updates\n```tsx\nconst queryClient = useQueryClient()\n\nconst mutation = useMutation({\n  mutationFn: updatePost,\n  onMutate: async (updatedPost) => {\n    await queryClient.cancelQueries({ queryKey: postKeys.detail(updatedPost.id) })\n    const previous = queryClient.getQueryData(postKeys.detail(updatedPost.id))\n    queryClient.setQueryData(postKeys.detail(updatedPost.id), updatedPost)\n    return { previous }\n  },\n  onError: (err, updatedPost, context) => {\n    queryClient.setQueryData(postKeys.detail(updatedPost.id), context?.previous)\n  },\n  onSettled: (_, __, updatedPost) => {\n    queryClient.invalidateQueries({ queryKey: postKeys.detail(updatedPost.id) })\n  },\n})\n```\n\n## Infinite Queries\n```tsx\nconst {\n  data,\n  fetchNextPage,\n  hasNextPage,\n  isFetchingNextPage,\n} = useInfiniteQuery({\n  queryKey: postKeys.lists(),\n  queryFn: ({ pageParam }) => fetchPosts({ cursor: pageParam }),\n  initialPageParam: undefined as string | undefined,\n  getNextPageParam: (lastPage) => lastPage.nextCursor,\n})\n\n// data.pages is an array of page results — flatten for rendering\nconst allPosts = data?.pages.flatMap((page) => page.items) ?? []\n```\n\n## Prefetching\n- Prefetch on hover or during routing to eliminate loading states\n```ts\n// Hover prefetch\nconst handleMouseEnter = () => {\n  queryClient.prefetchQuery(postQueryOptions(postId))\n}\n\n// In router loader (eliminates all loading spinners)\nexport const Route = createFileRoute('/posts/$postId')({\n  loader: ({ context: { queryClient }, params }) =>\n    queryClient.ensureQueryData(postQueryOptions(params.postId)),\n})\n```\n\n## Cache Invalidation Patterns\n```ts\n// Invalidate all post queries\nqueryClient.invalidateQueries({ queryKey: postKeys.all })\n\n// Invalidate only post lists\nqueryClient.invalidateQueries({ queryKey: postKeys.lists() })\n\n// Remove from cache entirely\nqueryClient.removeQueries({ queryKey: postKeys.detail(id) })\n\n// Directly update cache without refetch\nqueryClient.setQueryData(postKeys.detail(id), newData)\n```\n\n## Suspense Mode\n- Use `useSuspenseQuery` for Suspense-based data fetching (v5)\n- Wrap with `<Suspense fallback={<Skeleton />}>`\n- Pair with `<ErrorBoundary>` for error handling\n```tsx\n// No need to handle isLoading — Suspense handles it\nconst { data } = useSuspenseQuery(postQueryOptions(postId))\n```\n\n## Performance Best Practices\n- Set appropriate `staleTime` per query — defaults to `0` (always stale)\n- Use `select` to transform/subscribe to only relevant slices of data\n- Use `placeholderData: keepPreviousData` for pagination to avoid layout shifts\n- Avoid creating `QueryClient` inside components — instantiate once at app root\n- Use `notifyOnChangeProps` to limit re-renders to only relevant data changes\n\n## Error Handling\n- Use `throwOnError: true` to bubble errors to the nearest ErrorBoundary\n- Use `retry` function for conditional retry logic (e.g., skip retry on 404)\n```ts\nretry: (failureCount, error) => {\n  if (error.status === 404) return false\n  return failureCount < 3\n},\n```\n\n## TypeScript Tips\n- Always type `queryFn` return value explicitly or infer from typed API functions\n- Use `QueryObserverResult<TData, TError>` to type hook return values\n- Use `UseMutationResult<TData, TError, TVariables>` for mutations\n"}