Доки по разработке
This project is maintained by teniryte
TanStack Query может переживать перезагрузку страницы, работать без сети и подготавливать данные на сервере.
import { QueryClient } from '@tanstack/react-query';
import { persistQueryClient } from '@tanstack/react-query-persist-client';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
const queryClient = new QueryClient();
persistQueryClient({
queryClient,
persister: createSyncStoragePersister({
storage: window.localStorage,
key: 'app-cache-v5',
throttleTime: 1000,
}),
maxAge: 24 * 60 * 60 * 1000, // 24h
hydrateOptions: {
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
},
},
},
dehydrateOptions: {
shouldDehydrateQuery: ({ state }) => state.status === 'success',
},
});
localStorage и восстанавливается при следующем запуске.buster (версию схемы), чтобы сбрасывать устаревшие данные.hydrateOptions/dehydrateOptions, чтобы выравнивать staleTime и сразу помечать критичные запросы как просроченные.throttleTime защищает сторедж от частых записей при бурных мутациях, а serialize/deserialize помогают компрессировать данные (например, через superjson).persistQueryClientRestore вручную при инициализации, а persistQueryClientSave — после входа пользователя.gcTime запросов должен быть ≥ maxAge.const shouldKeep = (query) => {
const isLargeList = query.queryHash.startsWith('products');
return !isLargeList; // example: не пишем тяжёлые списки
};
persistQueryClient({
queryClient,
persister,
dehydrateOptions: {
shouldDehydrateQuery: shouldKeep,
},
});
serializeError/deserializeError, если хотите ресурфейсить сообщения без дополнительного запроса.createIDBPersister (из @tanstack/query-async-storage-persister), чтобы не упереться в лимит localStorage.persistQueryClient в слой шифрования (например, crypto.subtle), но помните, что дешифровка должна быть синхронной или выполнена до hydrate.import AsyncStorage from '@react-native-async-storage/async-storage';
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
const persister = createAsyncStoragePersister({
storage: AsyncStorage,
retry: async (err) => {
await wait(1000);
return err?.message !== 'QuotaExceededError';
},
});
maxAge, иначе iOS может удалить приложение и сторедж ради места.MMKV (react-native-mmkv), но тогда потребуется собственный persister.retry с бэкоффом, чтобы переживать QuotaExceededError и временно отключённый Secure Storage.AppState для очередей: как только приложение вновь активно, вызывайте persistQueryClientRestore.import { onlineManager, focusManager } from '@tanstack/react-query';
onlineManager.setEventListener(setOnline => {
window.addEventListener('online', () => setOnline(true));
window.addEventListener('offline', () => setOnline(false));
});
focusManager.setEventListener(handleFocus => {
window.addEventListener('visibilitychange', () => handleFocus(!document.hidden));
});
window.addEventListener, но вы можете адаптировать к Electron/React Native.onlineManager сообщает о «офлайне», все запросы переходят в fetchStatus: 'paused' и автоматически продолжатся после восстановления связи.@react-native-community/netinfo и AppState, чтобы сообщать onlineManager о состоянии сети и активности экрана.onlineManager.setOnline.onlineManager.subscribe(cb) и ручного queryClient.invalidateQueries, как только сеть вернулась.focusManager.setFocused(true) и возьмите ответственность за энергопотребление.import { persistQueryClientRestore } from '@tanstack/react-query-persist-client';
await persistQueryClientRestore({
queryClient,
persister,
maxAge: Infinity,
});
// useMutation({ networkMode: 'offlineFirst' });
networkMode: 'offlineFirst' заставляет мутацию помещаться в очередь и выполняться после возвращения сети.
networkMode: 'always' полезен для сервисов, которые обязаны синхронизироваться даже в фоне (например, через Service Worker), но в браузере он будет кидать ошибки при недоступной сети.meta: { offlineRetry: true } и глобальный mutationCache, чтобы логировать и визуализировать очередь.const mutationCache = new MutationCache({
onSuccess: (_data, variables, _ctx, mutation) => {
if (mutation.meta?.flushOnReconnect) {
queryClient.invalidateQueries(['invoices']);
}
},
});
const queryClient = new QueryClient({ mutationCache });
useMutation(updateInvoice, {
networkMode: 'offlineFirst',
meta: { flushOnReconnect: true },
});
mutation.meta и восстанавливайте UI, если пользователь закрыл вкладку до синхронизации.sync событии Service Worker извлекает мутации и выполняет их даже когда вкладка закрыта.persistQueryClientRestore вызовите queryClient.resumePausedMutations(), иначе мутации останутся в paused.queryClient.invalidateQueries({ predicate }), пока приложение находится в «сплэше».isRestoring из своего стора и рендерьте скелет до hydrate.QueryClient на сервере.prefetchQuery/prefetchInfiniteQuery для нужных данных.dehydrate(queryClient).Hydrate.export async function render(url: string) {
const queryClient = new QueryClient();
await queryClient.prefetchQuery(appBootstrapQuery());
const html = renderToString(
<QueryClientProvider client={queryClient}>
<Hydrate state={dehydrate(queryClient)}>
<App url={url} />
</Hydrate>
</QueryClientProvider>,
);
return html;
}
AbortController: если пользователь прервал HTTP-запрос, отмените и соответствующий prefetchQuery, чтобы избежать утечек.prefetchInfiniteQuery передайте initialPageParam, иначе первая страница может отличаться от клиента.dehydrate принимает shouldDehydrateQuery: можно исключить приватные данные и мутации, которые не должны попадать в HTML.defaultOptions.queries.gcTime = Infinity на сервере, чтобы сборщик мусора не удалил результаты до момента dehydrate.QueryClientProvider внутри Providers.dehydrate на уровне маршрута и прокидывайте через HydrationBoundary.react-cache/cache() из Next.js.generateStaticParams и generateMetadata можно вызывать prefetchQuery, но не забудьте создать независимый QueryClient, чтобы не смешивать данные разных запросов.server actions) держите QueryClient короткоживущим: создавайте его прямо внутри действия, чтобы не тащить состояние между пользователями.HydrationBoundary state={dehydrate(...)} и добавляйте suspense: true, чтобы использовать React Streaming.HydrationBoundary, это уменьшит размер HTML и ускорит LCP.loader создайте QueryClient, выполните prefetchQuery и верните dehydrate.Route используйте useLoaderData + Hydrate.shouldRevalidate = false, если данные полностью покрываются react-query и вам не нужен повторный вызов loader.action возвращайте mutationCache состояние, чтобы клиент знал, что мутация уже выполнена на сервере и не запускал её повторно.serializeError/deserializeError, если хотите сохранять ошибки.localStorage: старые записи можно вручную удалять через queryClient.removeQueries.queryClient.clear() + persister.removeClient().sessionStorage (меньше риск XSS) или используйте ServiceWorker с шифрованием.paused запросы.onlineManager.isOnline(), чтобы сообщать пользователю о состоянии сети.queryClient.getQueryCache().findAll({ fetchStatus: 'paused' }), чтобы видеть, какие ключи застряли офлайн.onReconnect хук (собственный) и запускайте refetchOnReconnect: false для шумных запросов, иначе при восстановлении сети можно DDOS-нуть API.service-worker.js можно подключить тот же persister (через IndexedDB) и восстанавливать кеш прямо в воркере, чтобы отдавать данные из postMessage ещё до загрузки React.queryClient.resumePausedMutations().workbox.precaching и persistQueryClient, чтобы инвалидировать кеш при развертывании.queryClient.prefetchQuery.hydrate прогоните queryClient.resumePausedMutations и invalidateQueries для «грязных» ключей — пользователь не увидит устаревший контент.onlineManager.isOnline() и либо показывает форму, либо ставит мутации в очередь.meta.analyticsKey во все запросы и отправляйте события, когда paused → fetching, чтобы отслеживать качество сети пользователей.