Интеграция TanStack Query с Next.js требует понимания различий между клиентским и серверным рендерингом. В обычном React-приложении запросы выполняются исключительно в браузере, тогда как Next.js поддерживает:
TanStack Query должен корректно работать во всех этих сценариях.
Основные задачи интеграции:
QueryClientДля Next.js используются стандартные пакеты:
npm install @tanstack/react-query
Для Devtools:
npm install @tanstack/react-query-devtools
Наиболее распространённая ошибка — создание нового
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,
},
},
})
}
Для 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>
)
}
После подключения провайдера запросы работают стандартным образом.
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 происходит следующий сценарий:
useQuery выполняет запросЭто ухудшает:
SSR позволяет загрузить данные заранее.
Для 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),
},
}
}
Кеш, полученный на сервере, должен быть восстановлен в браузере.
// 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(queryClient)
преобразует внутренний кеш в сериализуемый JSON.
Структура включает:
После передачи в браузер HydrationBoundary
восстанавливает кеш.
После гидратации TanStack Query может повторно выполнить запрос.
Причина — данные считаются stale.
Решение:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
})
Теперь данные остаются свежими 60 секунд.
Для SSR рекомендуется увеличенный staleTime.
Типичные значения:
| Тип данных | staleTime |
|---|---|
| Новости | 30–60 секунд |
| Профиль пользователя | 5–10 минут |
| Справочники | 1 час |
| Редко изменяемые данные | Infinity |
TanStack Query отлично подходит для статической генерации.
export async function getStaticProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
})
return {
props: {
dehydratedState: dehydrate(queryClient),
},
revalidate: 60,
}
}
Incremental Static Regeneration позволяет:
TanStack Query продолжает работать поверх ISR как обычный клиентский кеш.
Начиная с Next.js 13 появился App Router:
app/
В App Router активно используются:
Архитектура интеграции отличается от Pages Router.
'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>
)
}
// app/layout.js
import Providers from './providers'
export default function RootLayout({ children }) {
return (
<html lang="ru">
<body>
<Providers>
{children}
</Providers>
</body>
</html>
)
}
Компоненты с 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>
)
}
Server Components не поддерживают:
Поэтому TanStack Query используется только внутри Client Components.
В 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>
)
}
Сервер Next.js обслуживает множество пользователей одновременно.
Если использовать глобальный singleton:
const queryClient = new QueryClient()
возникают проблемы:
Поэтому для SSR создаётся новый экземпляр на каждый request.
В App Router fetch() по умолчанию кешируется.
fetch(url)
может вести себя неожиданно.
fetch(url, {
cache: 'no-store',
})
fetch(url, {
next: {
revalidate: 60,
},
})
Существует два уровня кеширования:
Неправильная настройка приводит к:
fetch(url, {
cache: 'no-store',
})
Управление выполняет TanStack Query.
fetch(url, {
next: {
revalidate: 300,
},
})
Next.js кеширует HTML, TanStack Query кеширует клиентские данные.
'use client'
import { ReactQueryDevtools }
from '@tanstack/react-query-devtools'
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
Devtools не должны попадать в production bundle.
import dynamic from 'next/dynamic'
const ReactQueryDevtools = dynamic(
() =>
import('@tanstack/react-query-devtools')
.then(mod => mod.ReactQueryDevtools),
{
ssr: false,
}
)
'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>
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
suspense: true,
})
<Suspense fallback={<Loader />}>
<Users />
</Suspense>
Next.js App Router поддерживает streaming SSR.
TanStack Query может использоваться совместно с:
Это позволяет постепенно отправлять HTML клиенту.
router.prefetch('/users')
await queryClient.prefetchQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
const results = useQueries({
queries: [
{
queryKey: ['users'],
queryFn: fetchUsers,
},
{
queryKey: ['posts'],
queryFn: fetchPosts,
},
],
})
SSR улучшает:
CSR-only подход хуже подходит для SEO.
Для 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()
}
import { headers } from 'next/headers'
const headersList = headers()
Для offline-кеша можно использовать:
npm install @tanstack/react-query-persist-client
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,
})
После выхода пользователя необходимо очищать кеш.
queryClient.clear()
или:
queryClient.removeQueries()
Для Next.js особенно важно:
['user', userId]
вместо:
['user']
Иначе возможны коллизии кеша при навигации.
Типичная архитектура:
src/
├── app/
├── components/
├── lib/
│ ├── query-client.js
│ ├── api.js
│ └── fetchers.js
├── hooks/
├── services/
└── providers/
export async function fetchUsers() {
const response = await fetch('/api/users')
if (!response.ok) {
throw new Error('Ошибка API')
}
return response.json()
}
// hooks/use-users.js
import { useQuery } from '@tanstack/react-query'
import { fetchUsers } from '../services/users'
export function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
}
В Next.js + TanStack Query TypeScript особенно полезен:
Неправильно:
function App() {
const queryClient = new QueryClient()
}
Следствие:
Следствие:
Ошибка архитектуры App Router.
Следствие:
SSR особенно полезен для:
CSR подходит для:
App Router предпочтителен для:
Pages Router остаётся удобным для: