-
-
Notifications
You must be signed in to change notification settings - Fork 3.3k
feat: added Next.js TanStack Query cursorrules #251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,103 @@ | ||||||||||||
| --- | ||||||||||||
| description: Next.js App Router combined with TanStack Query v5 — HydrationBoundary pattern, Server Actions as mutations, optimistic updates, and infinite scroll | ||||||||||||
| globs: ["app/**/*.tsx", "app/**/*.ts", "src/app/**/*.tsx", "src/queries/**/*"] | ||||||||||||
| alwaysApply: false | ||||||||||||
| --- | ||||||||||||
|
|
||||||||||||
| You are an expert in Next.js App Router, TanStack Query v5, TypeScript, and combining server components with client-side data fetching. | ||||||||||||
|
|
||||||||||||
| ## Architecture | ||||||||||||
| - Server Components fetch data directly — no TanStack Query needed there | ||||||||||||
| - TanStack Query lives in Client Components for interactive, real-time, or mutation-driven data | ||||||||||||
| - Hydrate the Query cache from server to avoid client waterfalls on first load | ||||||||||||
| - Use React Server Components for initial page data; TanStack Query for mutations + polling + optimistic UI | ||||||||||||
|
|
||||||||||||
| ## Provider Setup | ||||||||||||
| ```tsx | ||||||||||||
| // providers/query-provider.tsx | ||||||||||||
| 'use client' | ||||||||||||
| export function QueryProvider({ children }: { children: React.ReactNode }) { | ||||||||||||
| const [queryClient] = useState(() => new QueryClient({ | ||||||||||||
| defaultOptions: { queries: { staleTime: 60 * 1000 } }, | ||||||||||||
| })) | ||||||||||||
| return ( | ||||||||||||
| <QueryClientProvider client={queryClient}> | ||||||||||||
| {children} | ||||||||||||
| <ReactQueryDevtools initialIsOpen={false} /> | ||||||||||||
| </QueryClientProvider> | ||||||||||||
| ) | ||||||||||||
| } | ||||||||||||
| ``` | ||||||||||||
|
|
||||||||||||
| ## Server Prefetch + HydrationBoundary Pattern | ||||||||||||
| ```tsx | ||||||||||||
| // app/posts/page.tsx (Server Component) | ||||||||||||
| export default async function PostsPage() { | ||||||||||||
| const queryClient = new QueryClient() | ||||||||||||
| await queryClient.prefetchQuery(postsQueryOptions()) | ||||||||||||
| return ( | ||||||||||||
| <HydrationBoundary state={dehydrate(queryClient)}> | ||||||||||||
| <PostsList /> | ||||||||||||
| </HydrationBoundary> | ||||||||||||
| ) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // app/posts/_components/posts-list.tsx (Client Component) | ||||||||||||
| 'use client' | ||||||||||||
| export function PostsList() { | ||||||||||||
| const { data: posts } = useQuery(postsQueryOptions()) // reads from pre-populated cache | ||||||||||||
| return <ul>{posts?.map(p => <li key={p.id}>{p.title}</li>)}</ul> | ||||||||||||
| } | ||||||||||||
| ``` | ||||||||||||
|
|
||||||||||||
| ## queryOptions Factory | ||||||||||||
| ```ts | ||||||||||||
| export const postsQueryOptions = (filters?: PostFilters) => | ||||||||||||
| queryOptions({ | ||||||||||||
| queryKey: ['posts', 'list', filters], | ||||||||||||
| queryFn: () => fetch('/api/posts').then(r => r.json()), | ||||||||||||
| }) | ||||||||||||
| ``` | ||||||||||||
|
|
||||||||||||
| ## Server Actions as mutationFn | ||||||||||||
| ```tsx | ||||||||||||
| // app/posts/actions.ts | ||||||||||||
| 'use server' | ||||||||||||
| export async function createPost(data: { title: string; body: string }) { | ||||||||||||
| const post = await db.post.create({ data }) | ||||||||||||
| revalidatePath('/posts') | ||||||||||||
| return post | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // usage in Client Component | ||||||||||||
| const mutation = useMutation({ | ||||||||||||
| mutationFn: createPost, | ||||||||||||
| onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts', 'list'] }), | ||||||||||||
| }) | ||||||||||||
| ``` | ||||||||||||
|
|
||||||||||||
| ## Optimistic Updates | ||||||||||||
| ```tsx | ||||||||||||
| const mutation = useMutation({ | ||||||||||||
| mutationFn: updatePost, | ||||||||||||
| onMutate: async (updated) => { | ||||||||||||
| await queryClient.cancelQueries({ queryKey: ['posts', 'detail', updated.id] }) | ||||||||||||
| const previous = queryClient.getQueryData(['posts', 'detail', updated.id]) | ||||||||||||
| queryClient.setQueryData(['posts', 'detail', updated.id], (old: Post) => ({ ...old, ...updated })) | ||||||||||||
|
||||||||||||
| queryClient.setQueryData(['posts', 'detail', updated.id], (old: Post) => ({ ...old, ...updated })) | |
| queryClient.setQueryData( | |
| ['posts', 'detail', updated.id], | |
| (old: Post | undefined) => (old ? { ...old, ...updated } : updated) | |
| ) |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,227 @@ | ||||||||||
| You are an expert in Next.js (App Router), TanStack Query v5, TypeScript, and combining server components with client-side data fetching. | ||||||||||
|
|
||||||||||
| # Next.js App Router + TanStack Query v5 Guidelines | ||||||||||
|
|
||||||||||
| ## Architecture Philosophy | ||||||||||
| - Server Components fetch data directly (no TanStack Query needed there) | ||||||||||
| - TanStack Query lives in Client Components for interactive, real-time, or user-triggered data | ||||||||||
| - Use React Server Components for initial page data; TanStack Query for mutations, polling, and optimistic updates | ||||||||||
| - Hydrate the Query cache from server to avoid client waterfalls on first load | ||||||||||
|
|
||||||||||
| ## Provider Setup with Hydration | ||||||||||
| ```tsx | ||||||||||
| // src/providers/query-provider.tsx | ||||||||||
| 'use client' | ||||||||||
| import { QueryClient, QueryClientProvider } from '@tanstack/react-query' | ||||||||||
| import { ReactQueryDevtools } from '@tanstack/react-query-devtools' | ||||||||||
| import { useState } from 'react' | ||||||||||
|
|
||||||||||
| export function QueryProvider({ children }: { children: React.ReactNode }) { | ||||||||||
| const [queryClient] = useState( | ||||||||||
| () => | ||||||||||
|
Comment on lines
+15
to
+21
|
||||||||||
| new QueryClient({ | ||||||||||
| defaultOptions: { | ||||||||||
| queries: { | ||||||||||
| staleTime: 60 * 1000, | ||||||||||
| retry: (count, error: any) => error?.status !== 404 && count < 2, | ||||||||||
|
||||||||||
| retry: (count, error: any) => error?.status !== 404 && count < 2, | |
| retry: (count) => count < 2, |
Copilot
AI
Apr 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The optimistic update uses queryClient.setQueryData(..., (old: Post) => ({ ...old, ...updated })). At runtime old can be undefined (e.g., if the detail query was never fetched), and spreading undefined will throw. Update the example to safely handle undefined (return old/initialize a default) and avoid typing old as always-present.
| queryClient.setQueryData(postKeys.detail(updated.id), (old: Post) => ({ ...old, ...updated })) | |
| queryClient.setQueryData(postKeys.detail(updated.id), (old: Post | undefined) => | |
| old ? { ...old, ...updated } : old, | |
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # Next.js App Router + TanStack Query v5 Cursor Rules | ||
|
|
||
| Cursor rules for combining Next.js App Router with TanStack Query v5 — covering the hydration pattern, Server Components vs Client Components data fetching strategy, Server Actions as mutation functions, and optimistic updates. | ||
|
|
||
| ## What's covered | ||
| - QueryProvider setup with per-session `QueryClient` | ||
| - Server-side prefetch + `HydrationBoundary` hydration pattern | ||
| - `queryOptions` factory pattern with key factories | ||
| - Next.js Server Actions as `mutationFn` in TanStack Query mutations | ||
| - Optimistic updates with rollback | ||
| - Infinite scroll with `useInfiniteQuery` | ||
| - When to use Server Components vs TanStack Query (decision table) | ||
| - Route Handler (API route) patterns for query targets | ||
|
|
||
| ## Author | ||
| Created by [usm4nhafeez](https://github.com/usm4nhafeez) | ||
|
|
||
| Contributed to [awesome-cursorrules](https://github.com/PatrickJS/awesome-cursorrules) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file appears to be a near-exact duplicate of
rules/nextjs-tanstack-query-cursorrules-prompt-file/nextjs-tanstack-query.mdc, which risks the two drifting over time—consider removing one or documenting which is the source of truth. Also,globsis expressed as a YAML array here, while mostrules-new/*.mdcuse a single comma-separated glob string (e.g.rules-new/react.mdc:3), so this entry may not be picked up by tooling expecting the common format.