Сериализация в TanStack Query используется для преобразования состояния кеша, запросов и данных в формат, пригодный для:
Основной механизм сериализации применяется через:
dehydratehydrateБез корректной сериализации невозможно безопасно передавать состояние
QueryClient между окружениями исполнения.
Функция dehydrate преобразует внутреннее состояние
QueryClient в обычный сериализуемый объект.
import { QueryClient, dehydrate } from '@tanstack/react-query'
const queryClient = new QueryClient()
const dehydratedState = dehydrate(queryClient)
Результат:
{
mutations: [],
queries: [
{
queryKey: ['posts'],
state: {
data: [...],
status: 'success',
dataUpdatedAt: 1716555555
}
}
]
}
Такой объект можно:
Функция hydrate восстанавливает кеш в новом экземпляре
QueryClient.
import { hydrate } from '@tanstack/react-query'
hydrate(queryClient, dehydratedState)
После гидратации:
import {
QueryClient,
dehydrate
} from '@tanstack/react-query'
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
const dehydratedState = dehydrate(queryClient)
return {
props: {
dehydratedState
}
}
import {
QueryClient,
QueryClientProvider,
HydrationBoundary
} from '@tanstack/react-query'
const queryClient = new QueryClient()
function App({ dehydratedState }) {
return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={dehydratedState}>
<PostsPage />
</HydrationBoundary>
</QueryClientProvider>
)
}
JSON поддерживает только ограниченный набор типов:
При сериализации теряются:
DateMapSetBigIntundefinedПример проблемы:
const data = {
createdAt: new Date()
}
JSON.stringify(data)
После восстановления:
{
createdAt: "2026-05-24T12:00:00.000Z"
}
Тип Date превращается в строку.
Часто используется ручное преобразование.
const serialize = (data) => ({
...data,
createdAt: data.createdAt.toISOString()
})
const deserialize = (data) => ({
...data,
createdAt: new Date(data.createdAt)
})
TanStack Query позволяет настраивать сериализацию при использовании persistence.
Пакет:
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
})
Теперь кеш автоматически:
const persister = createSyncStoragePersister({
storage: window.localStorage,
serialize: (data) => {
return JSON.stringify(data)
},
deserialize: (data) => {
return JSON.parse(data)
}
})
Обычный JSON плохо работает со сложными типами. Для решения часто
используется библиотека superjson.
Установка:
npm install superjson
import superjson from 'superjson'
const persister = createSyncStoragePersister({
storage: window.localStorage,
serialize: (data) => {
return superjson.stringify(data)
},
deserialize: (data) => {
return superjson.parse(data)
}
})
Корректно сериализуются:
DateMapSetBigIntПример:
const data = {
createdAt: new Date(),
ids: new Set([1, 2, 3])
}
После восстановления типы сохраняются.
Для больших кешей LocalStorage становится недостаточным.
Ограничения LocalStorage:
Для крупных приложений применяется IndexedDB.
npm install @tanstack/query-async-storage-persister
import {
createAsyncStoragePersister
} from '@tanstack/query-async-storage-persister'
const persister = createAsyncStoragePersister({
storage: indexedDBStorage
})
Не всегда необходимо сохранять весь кеш.
Для этого используется shouldDehydrateQuery.
const dehydratedState = dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
return query.queryKey[0] !== 'admin'
}
})
Запросы admin не попадут в сериализованное
состояние.
const dehydratedState = dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
return query.state.status === 'success'
}
})
По умолчанию mutations обычно не сериализуются для SSR.
Но persistence может сохранять их.
const dehydratedState = dehydrate(queryClient, {
shouldDehydrateMutation: (mutation) => {
return mutation.state.status === 'pending'
}
})
Это особенно важно для offline-first приложений.
Сценарий:
const queryClient = new QueryClient({
defaultOptions: {
mutations: {
retry: 3
}
}
})
Большие объёмы кеша приводят к:
const dehydratedState = dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
return query.queryKey[0] === 'critical'
}
})
Иногда API возвращает огромные структуры:
{
id: 1,
title: 'Post',
hugeBlob: '...'
}
Перед сериализацией можно трансформировать данные:
const cleanData = (data) => ({
...data,
hugeBlob: undefined
})
Hydration mismatch возникает, когда:
Проблемный вариант:
queryKey: ['posts', new Date()]
После сериализации:
['posts', '2026-05-24T00:00:00.000Z']
Ключ становится другим.
queryKey: ['posts', date.toISOString()]
Infinite queries содержат сложную структуру:
{
pages: [],
pageParams: []
}
TanStack Query сериализует их автоматически.
await queryClient.prefetchInfiniteQuery({
queryKey: ['feed'],
queryFn: fetchFeed,
initialPageParam: 0
})
const dehydratedState = dehydrate(queryClient)
На клиенте:
hydrate(queryClient, dehydratedState)
Структура страниц полностью сохраняется.
После восстановления кеш может немедленно считаться stale.
Решение:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5
}
}
})
Сериализованный кеш может быстро удаляться garbage collector’ом.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60
}
}
})
После деплоя структура данных может измениться.
Старый кеш становится несовместимым.
Используется versioning.
persistQueryClient({
queryClient,
persister,
buster: 'v2'
})
После изменения версии старый кеш автоматически удаляется.
persistQueryClient({
queryClient,
persister,
maxAge: 1000 * 60 * 60 * 24
})
Кеш старше 24 часов будет отброшен.
Повреждённый кеш может вызывать падение приложения.
deserialize: (cached) => {
try {
return JSON.parse(cached)
} catch {
return undefined
}
}
Иногда кеш содержит чувствительные данные.
Перед сохранением применяется шифрование.
serialize: async (data) => {
const json = JSON.stringify(data)
return encrypt(json)
},
deserialize: async (encrypted) => {
const json = await decrypt(encrypted)
return JSON.parse(json)
}
В React Native обычно используется AsyncStorage.
npm install @react-native-async-storage/async-storage
import AsyncStorage from '@react-native-async-storage/async-storage'
const persister = createAsyncStoragePersister({
storage: AsyncStorage
})
Сериализация также используется при синхронизации вкладок.
npm install @tanstack/query-broadcast-client-experimental
broadcastQueryClient({
queryClient,
broadcastChannel: 'app-cache'
})
Состояние запросов сериализуется и отправляется между вкладками браузера.
TanStack Query Devtools отображает:
Это помогает диагностировать:
На больших приложениях сериализация может становиться дорогой операцией.
Особенно при:
persistQueryClient({
queryClient,
persister,
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
return query.state.status === 'success'
}
}
})
Не рекомендуется сериализовать:
Подходящие данные:
Типичная production-конфигурация включает:
import { QueryClient } from '@tanstack/react-query'
import {
persistQueryClient
} from '@tanstack/react-query-persist-client'
import {
createSyncStoragePersister
} from '@tanstack/query-sync-storage-persister'
import superjson from 'superjson'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
gcTime: 1000 * 60 * 60
}
}
})
const persister = createSyncStoragePersister({
storage: window.localStorage,
serialize: (data) => {
return superjson.stringify(data)
},
deserialize: (data) => {
return superjson.parse(data)
}
})
persistQueryClient({
queryClient,
persister,
buster: 'v3',
maxAge: 1000 * 60 * 60 * 24,
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
return query.state.status === 'success'
}
}
})