PluginBench
Rule

nextjs tanstack query

via PatrickJS/awesome-cursorrules

Next.js App Router + TanStack Query v5 integration with HydrationBoundary, Server Actions, and optimistic updates.

What is nextjs tanstack query?

Combines Next.js Server Components with TanStack Query v5 for efficient data fetching and mutations. Use this rule when building interactive Next.js apps that need server-side prefetching, client-side caching, Server Action mutations, and optimistic UI updates without client waterfalls.

  • Prefetch data in Server Components and hydrate the Query cache to eliminate client-side loading spinners
  • Use Server Actions as type-safe mutation functions with automatic cache invalidation
  • Implement optimistic updates with rollback on error using onMutate/onError/onSettled lifecycle
  • Set up QueryClient provider with sensible defaults (staleTime, retry logic) and React Query DevTools
  • Define reusable query options with hierarchical cache keys for lists, details, and filtered queries
  • Support infinite queries and pagination with cursor-based fetching

Applies to

File patterns this rule matches.

**/*
Rule definition (reference)

Source of truth, from the repository.

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

// 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 (
    <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
// 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

// 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
// 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

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 WhenUse TanStack Query When
Static or rarely-changing dataReal-time or frequently-updated data
SEO-critical initial contentUser interactions (forms, toggles)
No need to refetch on clientOptimistic updates needed
Data is not shared across componentsData is shared across many components
No loading states desiredFine-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
// 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)

'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'

Related rules

Senior full-stack TypeScript, React, Node.js guidance with clean architecture, testing, and WHY-oriented reasoning.

**/*
41k
via PatrickJS/awesome-cursorrules

Quantitative factor research skills for designing, evaluating, and mining alpha factors in equities markets.

**/*
41k
via PatrickJS/awesome-cursorrules

Android development with Jetpack Compose, clean architecture, and Material Design 3.

**/*
41k
via PatrickJS/awesome-cursorrules

Angular development with Novo Elements UI library using standalone components.

**/*
41k
via PatrickJS/awesome-cursorrules

Expert Angular 18 + TypeScript development with Jest, emphasizing clean code and performance.

**/*
41k
via PatrickJS/awesome-cursorrules

Manage Kubernetes clusters, add-ons, stacks, and credentials via the Ankra CLI platform.

**/*.sh +5
41k
via PatrickJS/awesome-cursorrules