From d5daa2f3a39bf1ce4042c7de44b490f0ef5386e3 Mon Sep 17 00:00:00 2001 From: usm4nhafeez Date: Mon, 13 Apr 2026 13:23:30 +0500 Subject: [PATCH] feat: add Next.js TanStack Query cursorrules --- README.md | 1 + rules-new/nextjs-tanstack-query.mdc | 103 ++++++++ .../.cursorrules | 227 ++++++++++++++++++ .../README.md | 18 ++ .../nextjs-tanstack-query.mdc | 103 ++++++++ 5 files changed, 452 insertions(+) create mode 100644 rules-new/nextjs-tanstack-query.mdc create mode 100644 rules/nextjs-tanstack-query-cursorrules-prompt-file/.cursorrules create mode 100644 rules/nextjs-tanstack-query-cursorrules-prompt-file/README.md create mode 100644 rules/nextjs-tanstack-query-cursorrules-prompt-file/nextjs-tanstack-query.mdc diff --git a/README.md b/README.md index 63c6a2ff..6aab5892 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/rules-new/nextjs-tanstack-query.mdc b/rules-new/nextjs-tanstack-query.mdc new file mode 100644 index 00000000..b8251c3b --- /dev/null +++ b/rules-new/nextjs-tanstack-query.mdc @@ -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 ( + + {children} + + + ) +} +``` + +## 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 ( + + + + ) +} + +// app/posts/_components/posts-list.tsx (Client Component) +'use client' +export function PostsList() { + const { data: posts } = useQuery(postsQueryOptions()) // reads from pre-populated cache + return +} +``` + +## 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 })) + 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` diff --git a/rules/nextjs-tanstack-query-cursorrules-prompt-file/.cursorrules b/rules/nextjs-tanstack-query-cursorrules-prompt-file/.cursorrules new file mode 100644 index 00000000..5d231acb --- /dev/null +++ b/rules/nextjs-tanstack-query-cursorrules-prompt-file/.cursorrules @@ -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( + () => + new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, + retry: (count, error: any) => error?.status !== 404 && count < 2, + }, + }, + }), + ) + + return ( + + {children} + + + ) +} + +// src/app/layout.tsx +import { QueryProvider } from '@/providers/query-provider' + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ) +} +``` + +## 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 ( + + + + ) +} + +// 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 +} +``` + +## 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 ( + + ) +} +``` + +## 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 })) + 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 ( +
+ {posts.map(post => )} + +
+ ) +} +``` + +## 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'` diff --git a/rules/nextjs-tanstack-query-cursorrules-prompt-file/README.md b/rules/nextjs-tanstack-query-cursorrules-prompt-file/README.md new file mode 100644 index 00000000..38846d9e --- /dev/null +++ b/rules/nextjs-tanstack-query-cursorrules-prompt-file/README.md @@ -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) diff --git a/rules/nextjs-tanstack-query-cursorrules-prompt-file/nextjs-tanstack-query.mdc b/rules/nextjs-tanstack-query-cursorrules-prompt-file/nextjs-tanstack-query.mdc new file mode 100644 index 00000000..b8251c3b --- /dev/null +++ b/rules/nextjs-tanstack-query-cursorrules-prompt-file/nextjs-tanstack-query.mdc @@ -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 ( + + {children} + + + ) +} +``` + +## 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 ( + + + + ) +} + +// app/posts/_components/posts-list.tsx (Client Component) +'use client' +export function PostsList() { + const { data: posts } = useQuery(postsQueryOptions()) // reads from pre-populated cache + return +} +``` + +## 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 })) + 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`