Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ By creating a `.cursorrules` file in your project's root directory, you can leve
- [Next.js (React, TypeScript)](./rules/nextjs-react-typescript-cursorrules-prompt-file/.cursorrules) - Cursor rules for Next.js development with React and TypeScript integration.
- [Next.js (SEO Development)](./rules/nextjs-seo-dev-cursorrules-prompt-file/.cursorrules) - Cursor rules for Next.js development with SEO optimization.
- [Next.js (Supabase Todo App)](./rules/nextjs-supabase-todo-app-cursorrules-prompt-file/.cursorrules) - Cursor rules for Next.js development with Supabase integration for a Todo app.
- [Next.js (TanStack Query v5)](./rules/nextjs-tanstack-query-cursorrules-prompt-file/.cursorrules) - Cursor rules for Next.js App Router with TanStack Query v5, covering the HydrationBoundary pattern, Server Actions as mutations, and optimistic updates.
- [Next.js (Tailwind, TypeScript)](./rules/nextjs-tailwind-typescript-apps-cursorrules-prompt/.cursorrules) - Cursor rules for Next.js development with Tailwind CSS and TypeScript integration.
- [Next.js (TypeScript App)](./rules/nextjs-typescript-app-cursorrules-prompt-file/.cursorrules) - Cursor rules for Next.js development with TypeScript integration.
- [Next.js (TypeScript)](./rules/nextjs-typescript-cursorrules-prompt-file/.cursorrules) - Cursor rules for Next.js development with TypeScript integration.
Expand Down
103 changes: 103 additions & 0 deletions rules-new/nextjs-tanstack-query.mdc
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
---

Comment on lines +3 to +6
Copy link

Copilot AI Apr 13, 2026

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, globs is expressed as a YAML array here, while most rules-new/*.mdc use 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.

Suggested change
globs: ["app/**/*.tsx", "app/**/*.ts", "src/app/**/*.tsx", "src/queries/**/*"]
alwaysApply: false
---
globs: "app/**/*.tsx, app/**/*.ts, src/app/**/*.tsx, src/queries/**/*"
alwaysApply: false
---
Note: This file mirrors `rules/nextjs-tanstack-query-cursorrules-prompt-file/nextjs-tanstack-query.mdc`, which should be treated as the source of truth. Keep the two files synchronized if either is updated.

Copilot uses AI. Check for mistakes.
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 }))
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The optimistic update example spreads old ({ ...old, ...updated }) but old can be undefined if the cache is empty; spreading undefined throws. Update the example to guard against undefined and avoid typing old as always-present.

Suggested change
queryClient.setQueryData(['posts', 'detail', updated.id], (old: Post) => ({ ...old, ...updated }))
queryClient.setQueryData(
['posts', 'detail', updated.id],
(old: Post | undefined) => (old ? { ...old, ...updated } : updated)
)

Copilot uses AI. Check for mistakes.
return { previous }
},
onError: (_, updated, ctx) => {
queryClient.setQueryData(['posts', 'detail', updated.id], ctx?.previous)
},
onSettled: (_, __, updated) => {
queryClient.invalidateQueries({ queryKey: ['posts', 'detail', updated.id] })
},
})
```

## Key Rules
- Create a **new** `QueryClient` per request in Server Components — never reuse across requests
- Create **one** `QueryClient` per browser session via `useState` in the provider
- Always wrap server-prefetched subtrees in `HydrationBoundary`
- Mark all components using TanStack Query hooks with `'use client'`
- Never call `fetch` directly in Client Components — always go through `queryFn`
227 changes: 227 additions & 0 deletions rules/nextjs-tanstack-query-cursorrules-prompt-file/.cursorrules
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
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Provider snippet, children is typed as React.ReactNode but React isn’t imported. In many TS/Next.js setups this will fail with “Cannot find namespace 'React'”. Prefer import type { PropsWithChildren } from 'react' (or ReactNode) and type the props accordingly, or add an explicit React type import in the example.

Copilot uses AI. Check for mistakes.
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
retry: (count, error: any) => error?.status !== 404 && count < 2,
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

retry: (count, error) => error?.status !== 404 ... assumes the thrown error has an HTTP status, but the fetch examples shown (fetch(...).then(r => r.json())) won’t throw on non-2xx responses and typically won’t provide status. Either adjust the example fetchers to throw with status when !r.ok, or change the retry predicate to match the actual error type you expect.

Suggested change
retry: (count, error: any) => error?.status !== 404 && count < 2,
retry: (count) => count < 2,

Copilot uses AI. Check for mistakes.
},
},
}),
)

return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}

// src/app/layout.tsx
import { QueryProvider } from '@/providers/query-provider'

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<QueryProvider>{children}</QueryProvider>
</body>
</html>
)
}
```

## Hydration Pattern (Server → Client Cache)
- Prefetch in Server Components, dehydrate state, rehydrate in client
- This eliminates client-side loading states on first render
```tsx
// src/app/posts/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
import { postsQueryOptions } from '@/queries/posts'
import { PostsList } from './_components/posts-list'

export default async function PostsPage() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery(postsQueryOptions())

return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostsList />
</HydrationBoundary>
)
}

// src/app/posts/_components/posts-list.tsx
'use client'
import { useQuery } from '@tanstack/react-query'
import { postsQueryOptions } from '@/queries/posts'

export function PostsList() {
// Reads from pre-populated cache — no loading spinner
const { data: posts } = useQuery(postsQueryOptions())
return <ul>{posts?.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}
```

## Query Definitions
```ts
// src/queries/posts.ts
import { queryOptions } from '@tanstack/react-query'

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,
}

export const postsQueryOptions = (filters?: PostFilters) =>
queryOptions({
queryKey: postKeys.list(filters),
queryFn: () => fetch(`/api/posts`).then(r => r.json()),
})

export const postDetailQueryOptions = (id: string) =>
queryOptions({
queryKey: postKeys.detail(id),
queryFn: () => fetch(`/api/posts/${id}`).then(r => r.json()),
staleTime: 1000 * 60 * 5,
})
```

## Server Actions + Mutations
- Use Next.js Server Actions as the `mutationFn` in TanStack Query mutations
- This gives you type-safe server mutations WITH optimistic update/rollback capabilities
```tsx
// src/app/posts/actions.ts
'use server'
import { revalidatePath } from 'next/cache'

export async function createPost(data: { title: string; body: string }) {
const post = await db.post.create({ data })
revalidatePath('/posts')
return post
}

// src/app/posts/_components/create-post-form.tsx
'use client'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { createPost } from '../actions'
import { postKeys } from '@/queries/posts'

export function CreatePostForm() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: postKeys.lists() })
},
})

return (
<button
onClick={() => mutation.mutate({ title: 'New Post', body: '...' })}
disabled={mutation.isPending}
>
{mutation.isPending ? 'Creating...' : 'Create Post'}
</button>
)
}
```

## Optimistic Updates with Server Actions
```tsx
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), (old: Post) => ({ ...old, ...updated }))
Copy link

Copilot AI Apr 13, 2026

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.

Suggested change
queryClient.setQueryData(postKeys.detail(updated.id), (old: Post) => ({ ...old, ...updated }))
queryClient.setQueryData(postKeys.detail(updated.id), (old: Post | undefined) =>
old ? { ...old, ...updated } : old,
)

Copilot uses AI. Check for mistakes.
return { previous }
},
onError: (_, updated, ctx) => {
queryClient.setQueryData(postKeys.detail(updated.id), ctx?.previous)
},
onSettled: (_, __, updated) => {
queryClient.invalidateQueries({ queryKey: postKeys.detail(updated.id) })
},
})
```

## When to Use Server Components vs TanStack Query
| Use Server Components When | Use TanStack Query When |
|---|---|
| Static or rarely-changing data | Real-time or frequently-updated data |
| SEO-critical initial content | User interactions (forms, toggles) |
| No need to refetch on client | Optimistic updates needed |
| Data is not shared across components | Data is shared across many components |
| No loading states desired | Fine-grained loading/error UI needed |

## Route Handlers (API Routes) as Query Targets
- Use `src/app/api/` route handlers as the API layer for TanStack Query fetchers
- Keep route handlers thin — just parse/validate input and call service layer
```ts
// src/app/api/posts/route.ts
import { NextResponse } from 'next/server'

export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const posts = await getPosts({ category: searchParams.get('category') })
return NextResponse.json(posts)
}
```

## Infinite Queries (Pagination / Infinite Scroll)
```tsx
'use client'
import { useInfiniteQuery } from '@tanstack/react-query'

export function InfinitePosts() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: postKeys.lists(),
queryFn: ({ pageParam }) =>
fetch(`/api/posts?cursor=${pageParam ?? ''}`).then(r => r.json()),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})

const posts = data?.pages.flatMap(p => p.items) ?? []

return (
<div>
{posts.map(post => <PostCard key={post.id} post={post} />)}
<button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
</div>
)
}
```

## Key Rules
- Create one `QueryClient` per request on the server side (inside Server Components)
- Create one `QueryClient` per browser session on the client (via `useState` in provider)
- Always use `HydrationBoundary` when passing server-prefetched data to client components
- Never call `fetch` inside Client Components directly — always go through `queryFn`
- Mark all components that use TanStack Query hooks with `'use client'`
18 changes: 18 additions & 0 deletions rules/nextjs-tanstack-query-cursorrules-prompt-file/README.md
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)
Loading
Loading