В приложениях с интенсивной работой с серверными данными важна возможность сохранить текущее состояние кеша и восстановить его позже. В TanStack Query этот механизм называется восстановлением состояния (state restoration) или персистентностью кеша.
Восстановление состояния применяется в нескольких сценариях:
TanStack Query позволяет сериализовать состояние
QueryClient, сохранить его во внешнем хранилище и позже
восстановить.
Состояние TanStack Query состоит из:
Пример внутреннего состояния:
{
queries: [
{
queryKey: ['users'],
state: {
data: [...],
status: 'success',
fetchStatus: 'idle',
dataUpdatedAt: 1716542000000
}
}
]
}
TanStack Query предоставляет функции:
dehydratehydrateimport { dehydrate } from '@tanstack/react-query'
const dehydratedState = dehydrate(queryClient)
dehydrate извлекает сериализуемое состояние клиента.
import { hydrate } from '@tanstack/react-query'
hydrate(queryClient, dehydratedState)
После вызова hydrate кеш восстанавливается.
import {
QueryClient,
dehydrate
} from '@tanstack/react-query'
const queryClient = new QueryClient()
function saveCache() {
const state = dehydrate(queryClient)
localStorage.setItem(
'APP_CACHE',
JSON.stringify(state)
)
}
import {
QueryClient,
hydrate
} from '@tanstack/react-query'
const queryClient = new QueryClient()
const cache = localStorage.getItem('APP_CACHE')
if (cache) {
hydrate(
queryClient,
JSON.parse(cache)
)
}
Для автоматизации восстановления существует пакет:
npm install @tanstack/react-query-persist-client
Дополнительно используются persister-адаптеры.
Библиотека содержит специальный provider:
import {
PersistQueryClientProvider
} from '@tanstack/react-query-persist-client'
Он автоматически:
npm install @tanstack/query-sync-storage-persister
import { createSyncStoragePersister }
from '@tanstack/query-sync-storage-persister'
const persister = createSyncStoragePersister({
storage: window.localStorage
})
import React from 'react'
import {
QueryClient,
QueryClientProvider
} from '@tanstack/react-query'
import {
PersistQueryClientProvider
} from '@tanstack/react-query-persist-client'
import {
createSyncStoragePersister
} from '@tanstack/query-sync-storage-persister'
const queryClient = new QueryClient()
const persister = createSyncStoragePersister({
storage: window.localStorage
})
export function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{
persister
}}
>
<Main />
</PersistQueryClientProvider>
)
}
Алгоритм работы:
TanStack Query поддерживает разные варианты хранения.
Используется для:
createSyncStoragePersister({
storage: window.localStorage
})
Подходит для:
npm install @tanstack/query-async-storage-persister
import { createAsyncStoragePersister }
from '@tanstack/query-async-storage-persister'
const persister = createAsyncStoragePersister({
storage: AsyncStorage
})
Большие кеши лучше хранить в IndexedDB.
Пример через localforage:
npm install localforage
import localforage from 'localforage'
const persister = createAsyncStoragePersister({
storage: localforage
})
Persist layer поддерживает maxAge.
persistOptions={{
persister,
maxAge: 1000 * 60 * 60 * 24
}}
Пример:
24 часа = 86400000 ms
Если кеш старее maxAge, восстановление не
произойдёт.
Для инвалидирования старых кешей используется
buster.
persistOptions={{
persister,
buster: 'v2'
}}
После изменения значения старый кеш будет удалён.
Это важно при:
Не все запросы должны попадать в persistent cache.
Используется shouldDehydrateQuery.
dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
return query.state.status === 'success'
}
})
Часто запрещено сохранять:
Пример:
dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
return query.queryKey[0] !== 'auth'
}
})
localStorage имеет ограничения.
Обычно:
5–10 MB
Большие кеши вызывают:
shouldDehydrateQuery: (query) =>
query.state.status === 'success'
query.queryKey[0] !== 'realtime'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 5
}
}
})
Иногда применяется compression.
Например:
npm install lz-string
import LZString from 'lz-string'
const state = dehydrate(queryClient)
const compressed =
LZString.compress(JSON.stringify(state))
localStorage.setItem('CACHE', compressed)
const compressed =
localStorage.getItem('CACHE')
const state = JSON.parse(
LZString.decompress(compressed)
)
hydrate(queryClient, state)
После восстановления данные могут:
Это определяется staleTime.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 1000 * 60 * 10
})
Если staleTime не истёк:
Даже восстановленный кеш может автоматически обновляться.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
refetchOnMount: true
})
После возврата на вкладку:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
refetchOnWindowFocus: true
})
Данные могут быть обновлены повторно.
TanStack Query умеет сохранять mutation state.
Это важно для:
Пример:
const mutation = useMutation({
mutationFn: createPost
})
После восстановления TanStack Query может продолжить pending mutations.
В оффлайн-режиме mutation может ждать восстановления сети.
networkMode: 'offlineFirst'
const mutation = useMutation({
mutationFn: sendMessage,
networkMode: 'offlineFirst'
})
Во время восстановления provider блокирует запросы до завершения hydration.
Это предотвращает:
TanStack Query предоставляет специальный хук:
import { useIsRestoring }
from '@tanstack/react-query'
const isRestoring = useIsRestoring()
if (isRestoring) {
return <Loader />
}
В SSR восстановление играет ключевую роль.
Схема:
dehydrate.hydrate.const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
const dehydratedState =
dehydrate(queryClient)
return {
props: {
dehydratedState
}
}
hydrate(queryClient, dehydratedState)
В современных версиях применяется:
import {
HydrationBoundary
} from '@tanstack/react-query'
<HydrationBoundary state={dehydratedState}>
<PostsPage />
</HydrationBoundary>
В React Native обычно используется:
AsyncStorage
import AsyncStorage
from '@react-native-async-storage/async-storage'
const persister =
createAsyncStoragePersister({
storage: AsyncStorage
})
PWA-приложения особенно выигрывают от persistent cache.
Преимущества:
Не все данные сериализуются корректно.
Проблемы возникают с:
{
createdAt: new Date()
}
После восстановления:
typeof createdAt === 'string'
Рекомендуется хранить:
Иногда кеш повреждается.
Пример:
try {
hydrate(queryClient, state)
} catch (error) {
localStorage.removeItem('CACHE')
}
Pending mutations могут повторяться автоматически.
retry: 3
gcTime определяет время жизни неиспользуемого кеша.
queries: {
gcTime: 1000 * 60 * 30
}
Даже восстановленные данные могут быть удалены garbage collector.
Persist cache можно комбинировать с broadcast sync.
Например:
npm install @tanstack/query-broadcast-client-experimental
import {
broadcastQueryClient
} from '@tanstack/query-broadcast-client-experimental'
broadcastQueryClient({
queryClient,
broadcastChannel: 'app-cache'
})
Изменения в одной вкладке:
REST
GraphQL
gRPC
3G
mobile
satellite internet
Где:
Вызывает:
Ошибка безопасности:
['auth']
['token']
['payment']
После обновления приложения возможны:
Пользователь получает устаревшие данные.
Например:
Это приводит к восстановлению неактуального состояния.