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?

Comprehensive guide for combining Next.js Server Components with TanStack Query v5 for efficient data fetching, caching, and mutations. Use this rule when building interactive Next.js applications that need real-time updates, optimistic UI, and server-side data prefetching without client-side waterfalls.

  • Set up QueryClient provider with sensible defaults and React Query DevTools
  • Implement HydrationBoundary pattern to prefetch data server-side and hydrate client cache
  • Define reusable query options with hierarchical query keys for posts, details, and filters
  • Integrate Next.js Server Actions as type-safe mutation functions with automatic revalidation
  • Build optimistic updates with onMutate/onError/onSettled lifecycle for instant UI feedback
  • Handle 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

Next.js and TypeScript setup for Autonomys network block explorer development.

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

Next.js + TypeScript development rules with shadcn/ui, Tailwind, and Python LLM integration.

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

Next.js + TypeScript + Tailwind CSS setup for Autonomys block explorer development.

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

Next.js + Vercel + Supabase stack guide for BA Copilot, a BPMN diagram assistant for business analysts.

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

Next.js + TypeScript + Vercel development with AI SDK RSC, middleware, and KV database integration.

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

Next.js 15, React 19, Vercel AI, and Tailwind CSS development guide with TypeScript best practices.

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