{"filename":"nextjs-tanstack-query-cursorrules-prompt-file.mdc","content":"---\ndescription: \"Cursor rules for Next.js App Router with TanStack Query v5, covering the HydrationBoundary pattern, Server Actions as mutations, and optimistic updates.\"\nglobs: **/*\nalwaysApply: false\n---\nYou are an expert in Next.js (App Router), TanStack Query v5, TypeScript, and combining server components with client-side data fetching.\n\n# Next.js App Router + TanStack Query v5 Guidelines\n\n## Architecture Philosophy\n- Server Components fetch data directly (no TanStack Query needed there)\n- TanStack Query lives in Client Components for interactive, real-time, or user-triggered data\n- Use React Server Components for initial page data; TanStack Query for mutations, polling, and optimistic updates\n- Hydrate the Query cache from server to avoid client waterfalls on first load\n\n## Provider Setup with Hydration\n```tsx\n// src/providers/query-provider.tsx\n'use client'\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { ReactQueryDevtools } from '@tanstack/react-query-devtools'\nimport { useState } from 'react'\n\nexport function QueryProvider({ children }: { children: React.ReactNode }) {\n  const [queryClient] = useState(\n    () =>\n      new QueryClient({\n        defaultOptions: {\n          queries: {\n            staleTime: 60 * 1000,\n            retry: (count, error: any) => error?.status !== 404 && count < 2,\n          },\n        },\n      }),\n  )\n\n  return (\n    <QueryClientProvider client={queryClient}>\n      {children}\n      <ReactQueryDevtools initialIsOpen={false} />\n    </QueryClientProvider>\n  )\n}\n\n// src/app/layout.tsx\nimport { QueryProvider } from '@/providers/query-provider'\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html>\n      <body>\n        <QueryProvider>{children}</QueryProvider>\n      </body>\n    </html>\n  )\n}\n```\n\n## Hydration Pattern (Server → Client Cache)\n- Prefetch in Server Components, dehydrate state, rehydrate in client\n- This eliminates client-side loading states on first render\n```tsx\n// src/app/posts/page.tsx (Server Component)\nimport { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'\nimport { postsQueryOptions } from '@/queries/posts'\nimport { PostsList } from './_components/posts-list'\n\nexport default async function PostsPage() {\n  const queryClient = new QueryClient()\n  await queryClient.prefetchQuery(postsQueryOptions())\n\n  return (\n    <HydrationBoundary state={dehydrate(queryClient)}>\n      <PostsList />\n    </HydrationBoundary>\n  )\n}\n\n// src/app/posts/_components/posts-list.tsx\n'use client'\nimport { useQuery } from '@tanstack/react-query'\nimport { postsQueryOptions } from '@/queries/posts'\n\nexport function PostsList() {\n  // Reads from pre-populated cache — no loading spinner\n  const { data: posts } = useQuery(postsQueryOptions())\n  return <ul>{posts?.map(p => <li key={p.id}>{p.title}</li>)}</ul>\n}\n```\n\n## Query Definitions\n```ts\n// src/queries/posts.ts\nimport { queryOptions } from '@tanstack/react-query'\n\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\nexport const postsQueryOptions = (filters?: PostFilters) =>\n  queryOptions({\n    queryKey: postKeys.list(filters),\n    queryFn: () => fetch(`/api/posts`).then(r => r.json()),\n  })\n\nexport const postDetailQueryOptions = (id: string) =>\n  queryOptions({\n    queryKey: postKeys.detail(id),\n    queryFn: () => fetch(`/api/posts/${id}`).then(r => r.json()),\n    staleTime: 1000 * 60 * 5,\n  })\n```\n\n## Server Actions + Mutations\n- Use Next.js Server Actions as the `mutationFn` in TanStack Query mutations\n- This gives you type-safe server mutations WITH optimistic update/rollback capabilities\n```tsx\n// src/app/posts/actions.ts\n'use server'\nimport { revalidatePath } from 'next/cache'\n\nexport async function createPost(data: { title: string; body: string }) {\n  const post = await db.post.create({ data })\n  revalidatePath('/posts')\n  return post\n}\n\n// src/app/posts/_components/create-post-form.tsx\n'use client'\nimport { useMutation, useQueryClient } from '@tanstack/react-query'\nimport { createPost } from '../actions'\nimport { postKeys } from '@/queries/posts'\n\nexport function CreatePostForm() {\n  const queryClient = useQueryClient()\n  const mutation = useMutation({\n    mutationFn: createPost,\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: postKeys.lists() })\n    },\n  })\n\n  return (\n    <button\n      onClick={() => mutation.mutate({ title: 'New Post', body: '...' })}\n      disabled={mutation.isPending}\n    >\n      {mutation.isPending ? 'Creating...' : 'Create Post'}\n    </button>\n  )\n}\n```\n\n## Optimistic Updates with Server Actions\n```tsx\nconst mutation = useMutation({\n  mutationFn: updatePost,\n  onMutate: async (updated) => {\n    await queryClient.cancelQueries({ queryKey: postKeys.detail(updated.id) })\n    const previous = queryClient.getQueryData(postKeys.detail(updated.id))\n    queryClient.setQueryData(postKeys.detail(updated.id), (old: Post) => ({ ...old, ...updated }))\n    return { previous }\n  },\n  onError: (_, updated, ctx) => {\n    queryClient.setQueryData(postKeys.detail(updated.id), ctx?.previous)\n  },\n  onSettled: (_, __, updated) => {\n    queryClient.invalidateQueries({ queryKey: postKeys.detail(updated.id) })\n  },\n})\n```\n\n## When to Use Server Components vs TanStack Query\n| Use Server Components When | Use TanStack Query When |\n|---|---|\n| Static or rarely-changing data | Real-time or frequently-updated data |\n| SEO-critical initial content | User interactions (forms, toggles) |\n| No need to refetch on client | Optimistic updates needed |\n| Data is not shared across components | Data is shared across many components |\n| No loading states desired | Fine-grained loading/error UI needed |\n\n## Route Handlers (API Routes) as Query Targets\n- Use `src/app/api/` route handlers as the API layer for TanStack Query fetchers\n- Keep route handlers thin — just parse/validate input and call service layer\n```ts\n// src/app/api/posts/route.ts\nimport { NextResponse } from 'next/server'\n\nexport async function GET(request: Request) {\n  const { searchParams } = new URL(request.url)\n  const posts = await getPosts({ category: searchParams.get('category') })\n  return NextResponse.json(posts)\n}\n```\n\n## Infinite Queries (Pagination / Infinite Scroll)\n```tsx\n'use client'\nimport { useInfiniteQuery } from '@tanstack/react-query'\n\nexport function InfinitePosts() {\n  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({\n    queryKey: postKeys.lists(),\n    queryFn: ({ pageParam }) =>\n      fetch(`/api/posts?cursor=${pageParam ?? ''}`).then(r => r.json()),\n    initialPageParam: undefined as string | undefined,\n    getNextPageParam: (lastPage) => lastPage.nextCursor,\n  })\n\n  const posts = data?.pages.flatMap(p => p.items) ?? []\n\n  return (\n    <div>\n      {posts.map(post => <PostCard key={post.id} post={post} />)}\n      <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage}>\n        {isFetchingNextPage ? 'Loading...' : 'Load More'}\n      </button>\n    </div>\n  )\n}\n```\n\n## Key Rules\n- Create one `QueryClient` per request on the server side (inside Server Components)\n- Create one `QueryClient` per browser session on the client (via `useState` in provider)\n- Always use `HydrationBoundary` when passing server-prefetched data to client components\n- Never call `fetch` inside Client Components directly — always go through `queryFn`\n- Mark all components that use TanStack Query hooks with `'use client'`\n"}