GadgetForge

GadgetForge

TanStack Query in Next.js


Next.js vs TanStack Query Caching

First, let's compare the caching mechanisms of Next.js and TanStack Query.

FeatureNext.js CachingTanStack Query Caching
ScopeServer-side and client-sidePrimarily client-side
Main PurposeOptimize page and API response performanceEfficiently manage server data and provide fast user experience
Caching TimingAt build time (SSG), request time (SSR)At data fetching time
Caching Mechanism- Static pages at build time
- Server-rendered pages cached
- API responses cached
- Query key-based caching
- Automatic refetching & synchronization
Data FreshnessRe-generate static pages periodically (revalidate)Automatic refetch (staleTime, cacheTime, refetchOnWindowFocus, etc.)
Data Mutation & SyncServer-side mutations & synchronizationMutations to update client cache & optimistic updates
Cache ControlHTTP cache headers for browser/CDN controlClient-side control (staleTime, cacheTime, etc.)
PersistenceData persistence via server responsesPossible persistence to local/session storage (persistQueryClient)

TanStack Query vs SWR

Both TanStack Query and SWR are React libraries that simplify data fetching, caching, and synchronization.

FeatureTanStack QuerySWR
Data Fetching & CachingAutomatic caching, reuse, background updatesAutomatic caching & revalidation for freshness
Server State SyncFlexible strategies & detailed configAutomatic revalidation on changes
Background UpdatesHighly configurableSupported via focus revalidation, etc.
Pagination & Infinite ScrollEasy hooks (useInfiniteQuery)useSWRInfinite hook
Query Cancellation & RetriesFull support for cancellation & retry managementRetries supported, cancellation limited
Ease of UseRich features, detailed control, devtoolsSimple, intuitive API, quick start

If you don't need fine-grained control over data fetching, SWR's simplicity might be better.

For complex data management, debugging (devtools), or advanced caching strategies, TanStack Query is often the stronger choice.

Note: React Query has been rebranded as TanStack Query (part of the TanStack ecosystem). react-query == tanstack-query.

Installing TanStack Query

 bash
# Core library
npm install @tanstack/react-query

# Devtools (optional, great for debugging)
npm install @tanstack/react-query-devtools

# ESLint plugin (if needed)
npm install -D @tanstack/eslint-plugin-query

The experimental @tanstack/react-query-next-experimental package is optional for advanced streaming/hydration scenarios but not required for most modern setups.

Choosing a Prefetching Strategy

There are two main approaches for prefetching in Next.js App Router:

  • initialData: Fetch data on the server and pass it as a prop to Client Components. Simple & fast setup, but can lead to prop drilling in deep trees.

  • <HydrationBoundary> (formerly <Hydrate> in v4): Prefetch on server, dehydrate the cache state, and rehydrate on client via <HydrationBoundary>. More powerful for complex apps; avoids prop drilling and works seamlessly with Suspense/streaming.

Terminology

  • Prop Drilling: Passing data manually through component props from parent to deep children.
  • Hydration: Next.js sends initial HTML first (static or SSR). Then JavaScript "hydrates" interactive parts (attaches event listeners, makes React manage the DOM). Only Client Components ('use client') get hydrated — Server Components do not.

'use client' means the component needs interactivity/browser APIs, not that it's rendered only on the client. All components render on the server first; Client Components then hydrate in the browser.

1. Setting Up QueryClientProvider

Wrap your app with QueryClientProvider (use a singleton on client, fresh on server).

 tsx
// app/providers.tsx
'use client'

import { useState } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000, // 1 minute - avoid immediate refetch
      },
    },
  })
}

let browserQueryClient: QueryClient | undefined = undefined

function getQueryClient() {
  if (typeof window === 'undefined') {
    // Server: always new instance
    return makeQueryClient()
  } else {
    // Client: singleton
    if (!browserQueryClient) browserQueryClient = makeQueryClient()
    return browserQueryClient
  }
}

export default function Providers({ children }: { children: React.ReactNode }) {
  const queryClient = getQueryClient()

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

Then wrap in root layout:

 tsx
// app/layout.tsx
import Providers from '@/providers'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

2. Using Queries in Client Components

 tsx
// app/page.tsx (or any Client Component)
'use client'

import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
import { Suspense } from 'react'

export default function Page() {
  const { data, isLoading } = useQuery({
    queryKey: ['todos'],
    queryFn: () => fetch('https://jsonplaceholder.typicode.com/todos').then(res => res.json()),
  })

  // Or with Suspense (v5+ preferred for streaming)
  // const { data: suspenseData } = useSuspenseQuery({ queryKey: ['todos'], queryFn: ... })

  return (
    <div>
      {isLoading ? 'Loading...' : (
        <ul>
          {data?.map((todo: any) => <li key={todo.id}>{todo.title}</li>)}
        </ul>
      )}

      <Suspense fallback={<p>Loading suspense part...</p>}>
        {/* <TodoList /> – can use useSuspenseQuery inside */}
      </Suspense>
    </div>
  )
}

For parallel fetches with Suspense, use useSuspenseQuery (v5+). In v4 it was useQueries.

Client-Side Streaming Hydration (Advanced/Optional)

For streaming data directly from useQuery / useSuspenseQuery in Client Components during SSR (experimental in some setups):

Install (if needed): npm i @tanstack/react-query-next-experimental

Wrap with ReactQueryStreamedHydration:

 tsx
// app/providers.tsx (modified)
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'

// ...

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

Then in Client Component:

 tsx
useQuery({
  queryKey: ['todos'],
  queryFn: ...,
  suspense: true, // Enables streaming
})

Note: Streaming hydration can hurt SEO (content arrives progressively). For most apps in 2026, prefer server prefetching + <HydrationBoundary> for SEO + performance. Use Server Components / Server Actions for initial data, and TanStack Query only for client-side interactivity (mutations, infinite scroll, optimistic UI, etc.).

If your app doesn't need complex client features (e.g., infinite scroll), Next.js native fetching (Server Components + fetch with caching) + Server Actions often suffices — no extra library needed.

Official docs (2026): https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr



Related Posts in Series
Collapse
  • 1. TanStack Query in Next.js