Настройка для Next.js

Интеграция TanStack Query с Next.js требует понимания различий между клиентским и серверным рендерингом. В обычном React-приложении запросы выполняются исключительно в браузере, тогда как Next.js поддерживает:

  • SSR — Server-Side Rendering
  • SSG — Static Site Generation
  • ISR — Incremental Static Regeneration
  • CSR — Client-Side Rendering
  • Streaming и React Server Components в App Router

TanStack Query должен корректно работать во всех этих сценариях.

Основные задачи интеграции:

  • создание единого QueryClient
  • предотвращение дублирующих запросов
  • гидратация кеша между сервером и клиентом
  • управление временем жизни кеша
  • корректная работа с App Router
  • поддержка prefetching
  • предотвращение утечек памяти на сервере

Установка зависимостей

Для Next.js используются стандартные пакеты:

npm install @tanstack/react-query

Для Devtools:

npm install @tanstack/react-query-devtools

Базовая настройка QueryClient

Создание QueryClient

Наиболее распространённая ошибка — создание нового QueryClient при каждом рендере компонента.

Правильная структура:

// lib/query-client.js

import { QueryClient } from '@tanstack/react-query'

export function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000,
        gcTime: 5 * 60 * 1000,
        retry: 1,
        refetchOnWindowFocus: false,
      },
    },
  })
}

Настройка Next.js Pages Router

Подключение QueryClientProvider

Для Pages Router используется _app.js.

// pages/_app.js

import { QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
import { makeQueryClient } from '../lib/query-client'

export default function App({ Component, pageProps }) {
  const [queryClient] = useState(() => makeQueryClient())

  return (
    <QueryClientProvider client={queryClient}>
      <Component {...pageProps} />
    </QueryClientProvider>
  )
}

Использование useQuery

После подключения провайдера запросы работают стандартным образом.

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

async function fetchUsers() {
  const response = await fetch('/api/users')

  if (!response.ok) {
    throw new Error('Ошибка загрузки')
  }

  return response.json()
}

export default function UsersPage() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

  if (isLoading) {
    return <div>Загрузка...</div>
  }

  if (error) {
    return <div>Ошибка</div>
  }

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>
          {user.name}
        </li>
      ))}
    </ul>
  )
}

SSR и гидратация

Проблема двойного запроса

Без SSR происходит следующий сценарий:

  1. Сервер рендерит пустой HTML
  2. Браузер загружает JS
  3. useQuery выполняет запрос
  4. Пользователь получает данные позже

Это ухудшает:

  • SEO
  • Time To Content
  • Largest Contentful Paint

SSR позволяет загрузить данные заранее.


Prefetching на сервере

Использование dehydrate

Для SSR TanStack Query сериализует кеш на сервере и передаёт его клиенту.

// pages/users.js

import {
  QueryClient,
  dehydrate,
  useQuery,
} from '@tanstack/react-query'

async function fetchUsers() {
  const response = await fetch('https://api.example.com/users')
  return response.json()
}

export async function getServerSideProps() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  }
}

Гидратация на клиенте

HydrationBoundary

Кеш, полученный на сервере, должен быть восстановлен в браузере.

// pages/_app.js

import {
  HydrationBoundary,
  QueryClientProvider,
} from '@tanstack/react-query'

import { useState } from 'react'
import { makeQueryClient } from '../lib/query-client'

export default function App({ Component, pageProps }) {
  const [queryClient] = useState(() => makeQueryClient())

  return (
    <QueryClientProvider client={queryClient}>
      <HydrationBoundary state={pageProps.dehydratedState}>
        <Component {...pageProps} />
      </HydrationBoundary>
    </QueryClientProvider>
  )
}

Как работает dehydrate

Функция:

dehydrate(queryClient)

преобразует внутренний кеш в сериализуемый JSON.

Структура включает:

  • queryKey
  • данные
  • timestamps
  • статус запроса
  • метаданные

После передачи в браузер HydrationBoundary восстанавливает кеш.


Предотвращение повторного запроса

После гидратации TanStack Query может повторно выполнить запрос.

Причина — данные считаются stale.

Решение:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000,
    },
  },
})

Теперь данные остаются свежими 60 секунд.


Настройка staleTime для SSR

Для SSR рекомендуется увеличенный staleTime.

Типичные значения:

Тип данных staleTime
Новости 30–60 секунд
Профиль пользователя 5–10 минут
Справочники 1 час
Редко изменяемые данные Infinity

Использование SSG

getStaticProps

TanStack Query отлично подходит для статической генерации.

export async function getStaticProps() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  })

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
    revalidate: 60,
  }
}

ISR и TanStack Query

Incremental Static Regeneration позволяет:

  • обновлять HTML в фоне
  • комбинировать CDN и динамические данные
  • снижать нагрузку на API

TanStack Query продолжает работать поверх ISR как обычный клиентский кеш.


Настройка App Router

Особенности Next.js App Router

Начиная с Next.js 13 появился App Router:

app/

В App Router активно используются:

  • React Server Components
  • streaming
  • async components
  • server actions

Архитектура интеграции отличается от Pages Router.


Создание провайдера

app/providers.js

'use client'

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

import { useState } from 'react'

export default function Providers({ children }) {
  const [queryClient] = useState(() =>
    new QueryClient({
      defaultOptions: {
        queries: {
          staleTime: 60 * 1000,
        },
      },
    })
  )

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

Подключение providers в layout

// app/layout.js

import Providers from './providers'

export default function RootLayout({ children }) {
  return (
    <html lang="ru">
      <body>
        <Providers>
          {children}
        </Providers>
      </body>
    </html>
  )
}

Использование useQuery в App Router

Компоненты с useQuery должны быть клиентскими.

'use client'

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

export default function Users() {
  const { data } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

  return (
    <div>
      {data?.length}
    </div>
  )
}

React Server Components и TanStack Query

Важное ограничение

Server Components не поддерживают:

  • useState
  • useEffect
  • useQuery

Поэтому TanStack Query используется только внутри Client Components.


Prefetching в App Router

Серверная загрузка данных

В App Router prefetching обычно выполняется в серверном компоненте.

// app/users/page.js

import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query'

import UsersClient from './users-client'

async function fetchUsers() {
  const response = await fetch(
    'https://api.example.com/users'
  )

  return response.json()
}

export default async function UsersPage() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

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

Клиентский компонент

// app/users/users-client.js

'use client'

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

async function fetchUsers() {
  const response = await fetch(
    'https://api.example.com/users'
  )

  return response.json()
}

export default function UsersClient() {
  const { data } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>
          {user.name}
        </li>
      ))}
    </ul>
  )
}

Почему нужен отдельный QueryClient на сервере

Сервер Next.js обслуживает множество пользователей одновременно.

Если использовать глобальный singleton:

const queryClient = new QueryClient()

возникают проблемы:

  • утечки данных между пользователями
  • shared cache
  • memory leaks
  • race conditions

Поэтому для SSR создаётся новый экземпляр на каждый request.


Настройка fetch в Next.js

Кеширование Next.js

В App Router fetch() по умолчанию кешируется.

fetch(url)

может вести себя неожиданно.


Отключение кеша Next.js

fetch(url, {
  cache: 'no-store',
})

Использование revalidate

fetch(url, {
  next: {
    revalidate: 60,
  },
})

Конфликт кешей Next.js и TanStack Query

Существует два уровня кеширования:

  1. кеш Next.js
  2. кеш TanStack Query

Неправильная настройка приводит к:

  • устаревшим данным
  • невозможности invalidate
  • непредсказуемому refetch

Рекомендации по кешированию

Полностью клиентский кеш

fetch(url, {
  cache: 'no-store',
})

Управление выполняет TanStack Query.


Гибридная модель

fetch(url, {
  next: {
    revalidate: 300,
  },
})

Next.js кеширует HTML, TanStack Query кеширует клиентские данные.


Devtools в Next.js

Подключение

'use client'

import { ReactQueryDevtools }
from '@tanstack/react-query-devtools'
<QueryClientProvider client={queryClient}>
  {children}

  <ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>

Динамический импорт Devtools

Devtools не должны попадать в production bundle.

import dynamic from 'next/dynamic'

const ReactQueryDevtools = dynamic(
  () =>
    import('@tanstack/react-query-devtools')
      .then(mod => mod.ReactQueryDevtools),
  {
    ssr: false,
  }
)

Error Boundaries

Интеграция с Next.js

'use client'

import {
  QueryErrorResetBoundary,
} from '@tanstack/react-query'

import { ErrorBoundary }
from 'react-error-boundary'
<QueryErrorResetBoundary>
  {({ reset }) => (
    <ErrorBoundary
      onRe set={reset}
      fallbackRender={({ resetErrorBoundary }) => (
        <div>
          Ошибка
          <button onCl ick={resetErrorBoundary}>
            Повторить
          </button>
        </div>
      )}
    >
      <Users />
    </ErrorBoundary>
  )}
</QueryErrorResetBoundary>

Suspense в Next.js

Использование suspense

const { data } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  suspense: true,
})

SuspenseBoundary

<Suspense fallback={<Loader />}>
  <Users />
</Suspense>

Streaming в App Router

Next.js App Router поддерживает streaming SSR.

TanStack Query может использоваться совместно с:

  • Suspense
  • async Server Components
  • partial rendering

Это позволяет постепенно отправлять HTML клиенту.


Prefetching при навигации

Использование router.prefetch

router.prefetch('/users')

PrefetchQuery вручную

await queryClient.prefetchQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
})

Параллельные запросы

useQueries

const results = useQueries({
  queries: [
    {
      queryKey: ['users'],
      queryFn: fetchUsers,
    },
    {
      queryKey: ['posts'],
      queryFn: fetchPosts,
    },
  ],
})

SEO и TanStack Query

SSR улучшает:

  • индексацию
  • social previews
  • скорость первого рендера
  • Core Web Vitals

CSR-only подход хуже подходит для SEO.


Настройка retry

Для SSR retry обычно отключают.

new QueryClient({
  defaultOptions: {
    queries: {
      retry: false,
    },
  },
})

Причина — сервер не должен многократно повторять неудачные запросы.


Обработка авторизации

На сервере необходимо передавать cookies.

async function fetchProfile(cookie) {
  const response = await fetch(
    'https://api.example.com/profile',
    {
      headers: {
        cookie,
      },
    }
  )

  return response.json()
}

Использование headers в App Router

import { headers } from 'next/headers'

const headersList = headers()

Persist Query Client

Для offline-кеша можно использовать:

npm install @tanstack/react-query-persist-client

Сохранение кеша в localStorage

import {
  persistQueryClient,
} from '@tanstack/react-query-persist-client'

import {
  createSyncStoragePersister,
} from '@tanstack/query-sync-storage-persister'
const persister = createSyncStoragePersister({
  storage: window.localStorage,
})

persistQueryClient({
  queryClient,
  persister,
})

Очистка кеша при logout

После выхода пользователя необходимо очищать кеш.

queryClient.clear()

или:

queryClient.removeQueries()

Разделение query keys

Для Next.js особенно важно:

['user', userId]

вместо:

['user']

Иначе возможны коллизии кеша при навигации.


Структура проекта

Типичная архитектура:

src/
├── app/
├── components/
├── lib/
│   ├── query-client.js
│   ├── api.js
│   └── fetchers.js
├── hooks/
├── services/
└── providers/

Выделение API слоя

services/users.js

export async function fetchUsers() {
  const response = await fetch('/api/users')

  if (!response.ok) {
    throw new Error('Ошибка API')
  }

  return response.json()
}

Переиспользуемые hooks

// hooks/use-users.js

import { useQuery } from '@tanstack/react-query'
import { fetchUsers } from '../services/users'

export function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })
}

Использование TypeScript

В Next.js + TanStack Query TypeScript особенно полезен:

  • типизация API
  • типизация queryFn
  • безопасная гидратация
  • автокомплит query keys
  • предотвращение runtime ошибок

Частые ошибки

Создание QueryClient внутри рендера

Неправильно:

function App() {
  const queryClient = new QueryClient()
}

Отсутствие staleTime после SSR

Следствие:

  • двойные запросы
  • лишний network traffic

Глобальный QueryClient на сервере

Следствие:

  • shared cache
  • утечки пользовательских данных

Использование useQuery в Server Components

Ошибка архитектуры App Router.


Смешивание кешей Next.js и TanStack Query без стратегии

Следствие:

  • рассинхронизация данных
  • неожиданные refetch
  • stale UI

Практическая схема работы

SSR страница

  1. Сервер создаёт QueryClient
  2. Выполняется prefetchQuery
  3. Кеш сериализуется через dehydrate
  4. HTML отправляется клиенту
  5. HydrationBoundary восстанавливает кеш
  6. useQuery получает данные мгновенно
  7. TanStack Query обновляет данные по staleTime

Когда SSR действительно нужен

SSR особенно полезен для:

  • SEO-страниц
  • карточек товаров
  • блогов
  • публичных профилей
  • маркетинговых страниц

Когда достаточно CSR

CSR подходит для:

  • админок
  • внутренних панелей
  • dashboard
  • систем мониторинга
  • интерактивных SPA

Когда использовать App Router

App Router предпочтителен для:

  • новых проектов
  • streaming SSR
  • Server Components
  • server actions
  • современной архитектуры React

Когда Pages Router всё ещё актуален

Pages Router остаётся удобным для:

  • старых проектов
  • постепенной миграции
  • простого SSR
  • совместимости с legacy кодом
  • более предсказуемой архитектуры SSR