Настройка сериализации

Сериализация в TanStack Query используется для преобразования состояния кеша, запросов и данных в формат, пригодный для:

  • передачи между сервером и клиентом;
  • хранения в persistent storage;
  • восстановления состояния приложения;
  • межвкладочной синхронизации;
  • SSR и hydration;
  • offline-first архитектуры;
  • кэширования в IndexedDB, LocalStorage или AsyncStorage.

Основной механизм сериализации применяется через:

  • dehydrate
  • hydrate
  • persisters
  • кастомные функции сериализации
  • storage adapters

Без корректной сериализации невозможно безопасно передавать состояние QueryClient между окружениями исполнения.


Базовая сериализация состояния 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
      }
    }
  ]
}

Такой объект можно:

  • преобразовать в JSON;
  • сохранить в LocalStorage;
  • передать через SSR;
  • отправить по сети.

Гидратация состояния

Восстановление кеша

Функция hydrate восстанавливает кеш в новом экземпляре QueryClient.

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

hydrate(queryClient, dehydratedState)

После гидратации:

  • запросы появляются в кеше;
  • компоненты получают данные мгновенно;
  • повторный fetch может не выполняться;
  • сохраняется состояние stale/fresh.

Настройка сериализации для SSR

Серверная часть

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-сериализации

JSON поддерживает только ограниченный набор типов:

  • string
  • number
  • boolean
  • null
  • object
  • array

При сериализации теряются:

  • Date
  • Map
  • Set
  • BigInt
  • классы
  • undefined
  • функции
  • circular references

Пример проблемы:

const data = {
  createdAt: new Date()
}

JSON.stringify(data)

После восстановления:

{
  createdAt: "2026-05-24T12:00:00.000Z"
}

Тип Date превращается в строку.


Кастомная сериализация данных

Использование transform-функций

Часто используется ручное преобразование.

const serialize = (data) => ({
  ...data,
  createdAt: data.createdAt.toISOString()
})

const deserialize = (data) => ({
  ...data,
  createdAt: new Date(data.createdAt)
})

Настройка serialize/deserialize через persister

TanStack Query позволяет настраивать сериализацию при использовании persistence.


PersistQueryClient

Пакет:

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

LocalStorage persistence

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

import {
  createSyncStoragePersister
} from '@tanstack/query-sync-storage-persister'

Создание persister

const persister = createSyncStoragePersister({
  storage: window.localStorage
})

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

persistQueryClient({
  queryClient,
  persister
})

Теперь кеш автоматически:

  • сериализуется;
  • сохраняется;
  • восстанавливается после перезагрузки страницы.

Кастомная сериализация persister

serialize и deserialize

const persister = createSyncStoragePersister({
  storage: window.localStorage,

  serialize: (data) => {
    return JSON.stringify(data)
  },

  deserialize: (data) => {
    return JSON.parse(data)
  }
})

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

Обычный 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)
  }
})

Преимущества superjson

Корректно сериализуются:

  • Date
  • Map
  • Set
  • BigInt
  • RegExp
  • undefined
  • nested complex structures

Пример:

const data = {
  createdAt: new Date(),
  ids: new Set([1, 2, 3])
}

После восстановления типы сохраняются.


Настройка сериализации для IndexedDB

Для больших кешей LocalStorage становится недостаточным.

Ограничения LocalStorage:

  • маленький объём;
  • синхронный API;
  • блокировка main thread.

Для крупных приложений применяется IndexedDB.


Async persister

npm install @tanstack/query-async-storage-persister

Настройка

import {
  createAsyncStoragePersister
} from '@tanstack/query-async-storage-persister'

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

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

По умолчанию mutations обычно не сериализуются для SSR.

Но persistence может сохранять их.


shouldDehydrateMutation

const dehydratedState = dehydrate(queryClient, {
  shouldDehydrateMutation: (mutation) => {
    return mutation.state.status === 'pending'
  }
})

Offline persistence mutations

Это особенно важно для offline-first приложений.

Сценарий:

  1. Пользователь создаёт mutation.
  2. Интернет пропадает.
  3. Mutation сохраняется.
  4. После восстановления сети mutation повторяется.

Настройка retry после восстановления

const queryClient = new QueryClient({
  defaultOptions: {
    mutations: {
      retry: 3
    }
  }
})

Ограничение размера сериализованного кеша

Большие объёмы кеша приводят к:

  • медленной гидратации;
  • росту памяти;
  • долгому JSON.parse;
  • увеличению bundle payload.

selective dehydration

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

Hydration mismatch возникает, когда:

  • сервер и клиент имеют разные данные;
  • сериализация повреждает типы;
  • timestamps отличаются;
  • query keys не совпадают.

Нестабильные query keys

Проблемный вариант:

queryKey: ['posts', new Date()]

После сериализации:

['posts', '2026-05-24T00:00:00.000Z']

Ключ становится другим.


Правильный вариант

queryKey: ['posts', date.toISOString()]

Сериализация бесконечных запросов

Infinite queries содержат сложную структуру:

{
  pages: [],
  pageParams: []
}

TanStack Query сериализует их автоматически.


Пример infinite query hydration

await queryClient.prefetchInfiniteQuery({
  queryKey: ['feed'],
  queryFn: fetchFeed,
  initialPageParam: 0
})

Гидратация

const dehydratedState = dehydrate(queryClient)

На клиенте:

hydrate(queryClient, dehydratedState)

Структура страниц полностью сохраняется.


Настройка staleTime после гидратации

После восстановления кеш может немедленно считаться stale.

Решение:

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

Управление gcTime

Сериализованный кеш может быстро удаляться garbage collector’ом.

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

Persisted cache busting

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

Старый кеш становится несовместимым.

Используется versioning.


Настройка buster

persistQueryClient({
  queryClient,
  persister,
  buster: 'v2'
})

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


Максимальный возраст кеша

persistQueryClient({
  queryClient,
  persister,
  maxAge: 1000 * 60 * 60 * 24
})

Кеш старше 24 часов будет отброшен.


Обработка ошибок десериализации

Повреждённый кеш может вызывать падение приложения.


Безопасный deserialize

deserialize: (cached) => {
  try {
    return JSON.parse(cached)
  } catch {
    return undefined
  }
}

Шифрование сериализованного состояния

Иногда кеш содержит чувствительные данные.

Перед сохранением применяется шифрование.


Пример с Crypto API

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

В React Native обычно используется AsyncStorage.

npm install @react-native-async-storage/async-storage

Настройка persister

import AsyncStorage from '@react-native-async-storage/async-storage'

const persister = createAsyncStoragePersister({
  storage: AsyncStorage
})

Broadcast synchronization

Сериализация также используется при синхронизации вкладок.


broadcastQueryClient

npm install @tanstack/query-broadcast-client-experimental

Настройка

broadcastQueryClient({
  queryClient,
  broadcastChannel: 'app-cache'
})

Состояние запросов сериализуется и отправляется между вкладками браузера.


Devtools и сериализация

TanStack Query Devtools отображает:

  • сериализованные query keys;
  • timestamps;
  • cache state;
  • hydration status;
  • persisted queries.

Это помогает диагностировать:

  • hydration mismatch;
  • stale cache;
  • corrupted persistence;
  • duplicated queries.

Производительность сериализации

На больших приложениях сериализация может становиться дорогой операцией.

Особенно при:

  • тысячах query;
  • больших response payload;
  • частом persistence;
  • сложных nested объектах.

Оптимизация частоты сохранения

persistQueryClient({
  queryClient,
  persister,

  dehydrateOptions: {
    shouldDehydrateQuery: (query) => {
      return query.state.status === 'success'
    }
  }
})

Исключение volatile данных

Не рекомендуется сериализовать:

  • временные токены;
  • websocket state;
  • UI state;
  • loading flags;
  • transient данные.

Что желательно сериализовать

Подходящие данные:

  • API responses;
  • списки;
  • профили;
  • настройки;
  • offline mutations;
  • paginated data;
  • SSR cache.

Архитектура production persistence

Типичная production-конфигурация включает:

  • selective dehydration;
  • custom serialization;
  • versioning;
  • maxAge;
  • error recovery;
  • storage abstraction;
  • staleTime tuning;
  • offline mutation recovery.

Production-конфигурация persistence

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'
    }
  }
})