{"filename":"nextjs-tanstack-query.mdc","content":"---\ndescription: \"Next.js App Router combined with TanStack Query v5 — HydrationBoundary pattern, Server Actions as mutations, optimistic updates, and infinite scroll\"\nglobs: [\"app/**/*.tsx\", \"app/**/*.ts\", \"src/app/**/*.tsx\", \"src/queries/**/*\"]\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## Architecture\n- Server Components fetch data directly — no TanStack Query needed there\n- TanStack Query lives in Client Components for interactive, real-time, or mutation-driven data\n- Hydrate the Query cache from server to avoid client waterfalls on first load\n- Use React Server Components for initial page data; TanStack Query for mutations + polling + optimistic UI\n\n## Provider Setup\n```tsx\n// providers/query-provider.tsx\n'use client'\nexport function QueryProvider({ children }: { children: React.ReactNode }) {\n  const [queryClient] = useState(() => new QueryClient({\n    defaultOptions: { queries: { staleTime: 60 * 1000 } },\n  }))\n  return (\n    <QueryClientProvider client={queryClient}>\n      {children}\n      <ReactQueryDevtools initialIsOpen={false} />\n    </QueryClientProvider>\n  )\n}\n```\n\n## Server Prefetch + HydrationBoundary Pattern\n```tsx\n// app/posts/page.tsx  (Server Component)\nexport default async function PostsPage() {\n  const queryClient = new QueryClient()\n  await queryClient.prefetchQuery(postsQueryOptions())\n  return (\n    <HydrationBoundary state={dehydrate(queryClient)}>\n      <PostsList />\n    </HydrationBoundary>\n  )\n}\n\n// app/posts/_components/posts-list.tsx  (Client Component)\n'use client'\nexport function PostsList() {\n  const { data: posts } = useQuery(postsQueryOptions())  // reads from pre-populated cache\n  return <ul>{posts?.map(p => <li key={p.id}>{p.title}</li>)}</ul>\n}\n```\n\n## queryOptions Factory\n```ts\nexport const postsQueryOptions = (filters?: PostFilters) =>\n  queryOptions({\n    queryKey: ['posts', 'list', filters],\n    queryFn: () => fetch('/api/posts').then(r => r.json()),\n  })\n```\n\n## Server Actions as mutationFn\n```tsx\n// app/posts/actions.ts\n'use server'\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// usage in Client Component\nconst mutation = useMutation({\n  mutationFn: createPost,\n  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts', 'list'] }),\n})\n```\n\n## Optimistic Updates\n```tsx\nconst mutation = useMutation({\n  mutationFn: updatePost,\n  onMutate: async (updated) => {\n    await queryClient.cancelQueries({ queryKey: ['posts', 'detail', updated.id] })\n    const previous = queryClient.getQueryData(['posts', 'detail', updated.id])\n    queryClient.setQueryData(['posts', 'detail', updated.id], (old: Post) => ({ ...old, ...updated }))\n    return { previous }\n  },\n  onError: (_, updated, ctx) => {\n    queryClient.setQueryData(['posts', 'detail', updated.id], ctx?.previous)\n  },\n  onSettled: (_, __, updated) => {\n    queryClient.invalidateQueries({ queryKey: ['posts', 'detail', updated.id] })\n  },\n})\n```\n\n## Key Rules\n- Create a **new** `QueryClient` per request in Server Components — never reuse across requests\n- Create **one** `QueryClient` per browser session via `useState` in the provider\n- Always wrap server-prefetched subtrees in `HydrationBoundary`\n- Mark all components using TanStack Query hooks with `'use client'`\n- Never call `fetch` directly in Client Components — always go through `queryFn`\n"}