Доки по разработке
This project is maintained by teniryte
Предзагрузка и гидратация позволяют отдавать пользователю готовые данные до первого рендера и уменьшать «белые экраны».
<script> и избежать двойного запроса.staleTime, тем быстрее TanStack Query решит перезагрузить даже дегидрированные данные — держите это в голове, чтобы не терять преимущество SSR.IntersectionObserver (пользователь почти дошёл скроллом);loader следующего route);prefetchQueryawait queryClient.prefetchQuery(articleQuery(slug));
Promise<void>, поэтому удобно использовать в роутерах (React Router loader, Next.js generateStaticParams).fetchQuery, но подавляет ошибки наружу и не меняет статус активных подписчиков.await queryClient.prefetchQuery({
queryKey: ['article', slug],
queryFn: () => fetchArticle(slug, { signal }),
staleTime: 5 * 60_000,
gcTime: 30 * 60_000,
meta: { source: 'hover' },
});
useQuery.signal из React Router loader / Remix loader, чтобы отменять запрос, если пользователь уходит со страницы до завершения.meta, чтобы в Devtools понимать, откуда взялись данные.// React Router v6.22+
export const articleLoader =
(queryClient: QueryClient) =>
async ({ params, request }) => {
const slug = params.slug!;
await queryClient.ensureQueryData({
...articleQuery(slug),
signal: request.signal,
});
return null;
};
ensureQueryData: он вернёт фактические данные и не сделает лишний запрос, если кеш свежий.Promise.all, пока родительский loader ещё исполняется.dehydrate / Hydrate// server.tsx
await queryClient.prefetchQuery(articleQuery(slug));
const dehydratedState = dehydrate(queryClient);
// client.tsx
<Hydrate state={dehydratedState}>
<App />
</Hydrate>
dehydrate сериализует состояние Query Cache.dehydratedState на клиент и оборачивайте приложение в Hydrate.staleTime.gcTime < 5s) или приватные (зависящие от cookie) данные.const dehydratedState = dehydrate(queryClient, {
shouldDehydrateQuery: query =>
query.state.status === 'success' &&
query.queryKey[0] !== 'debug-only',
});
HydrationBoundary, чтобы догружать тяжёлые блоки лениво и избегать массивных JSON в html.<HydrationBoundary state={headerState}>
<Header />
</HydrationBoundary>
<HydrationBoundary state={contentState}>
<Article />
</HydrationBoundary>
initialDataUpdatedAt и удлиняйте staleTime, иначе сразу после гидратации произойдёт refetch и пользователь всё равно увидит двойной запрос.@tanstack/react-query-next-experimental и HydrationBoundary.defer + useLoaderData, затем useQuery с initialData.prefetchQuery прямо в серверном компонентах и обернуть клиентский компонент в HydrationBoundary:// app/(blog)/[slug]/page.tsx
export default async function Page({ params }) {
const queryClient = new QueryClient();
await queryClient.prefetchQuery(articleQuery(params.slug));
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ArticleClient slug={params.slug} />
</HydrationBoundary>
);
}
revalidate = 0 (ISR) нужно синхронизировать staleTime и revalidate: если staleTime слишком большой, клиент не увидит новые данные даже после регенерации страницы.defer({ dehydratedState }), а на клиенте вызвать useLoaderData и HydrationBoundary. Это уменьшит время TTFB, потому что остальная страница может стримиться сразу.initialData vs placeholderDatainitialData помечает запрос как status: 'success' мгновенно.placeholderData показывает временное значение, но статус остаётся pending.useQuery({
...articleQuery(slug),
initialData: () => queryClient.getQueryData(['articles-list'])?.find(a => a.slug === slug),
initialDataUpdatedAt: () => queryClient.getQueryState(['articles-list'])?.dataUpdatedAt,
});
Таким образом дата обновления совпадает с исходной записью, и staleTime учитывается корректно.
initialData отлично сочетается с Infinity Scroll: можно взять карточку из списка и мгновенно показать страницу детали.placeholderData полезен для скелетонов: верните облегчённую версию объекта (без тяжёлых полей), чтобы перейти от списка к деталям без «перескока».placeholderData не снимает suspense, а вот initialData позволит обойтись без fallback.placeholderData — функция, не забывайте проверять previousData, чтобы не затирать уже отображаемые значения.ensureQueryDataconst article = await queryClient.ensureQueryData(articleQuery(slug));
prefetchQuery, но возвращает данные (как fetchQuery), при этом не бросает ошибку, если данные уже загружены и свежие.ensureQueryData в Promise.all, чтобы сервер мог параллельно подтянуть независимые блоки:await Promise.all([
queryClient.ensureQueryData(profileQuery(userId)),
queryClient.ensureQueryData(recommendationsQuery(userId)),
]);
const prefetchArticle = (slug: string) => {
queryClient.prefetchQuery(articlePreviewQuery(slug), {
staleTime: 60_000,
});
};
<Link onMouseEnter={() => prefetchArticle(article.slug)} />
Пользователь получит мгновенный рендер карточки.
onTouchStart, иначе пользователь не успеет «нагреть» кеш.mousemove: оборачивайте обработчик в lodash/throttle или requestIdleCallback.p-limit), чтобы не DDOS-ить API.const observer = new IntersectionObserver(entries => {
entries
.filter(entry => entry.isIntersecting)
.forEach(entry => {
const slug = entry.target.getAttribute('data-slug')!;
queryClient.prefetchQuery(articlePreviewQuery(slug));
});
});
<script><script id="__INITIAL_DATA__" type="application/json">
{"dehydratedState": {...}}
</script>
На клиенте:
const dehydratedState = JSON.parse(
document.getElementById('__INITIAL_DATA__')!.textContent!,
);
const queryClient = new QueryClient();
hydrate(queryClient, dehydratedState);
dehydratedState перед вставкой (например, JSON.stringify(state, (_, value) => value ?? undefined)), чтобы убрать undefined и сократить payload.serialize-javascript или devalue.placeholderDatauseQuery({
...productQuery(id),
placeholderData: previousData => previousData ?? skeletonProduct,
});
placeholderData не попадает в кеш. Если нужно сохранить его в кеше, комбинируйте placeholderData с onSuccess, где вручную вызовете setQueryData.placeholderData из «соседних» страниц, чтобы при переходе вперёд/назад данные не мигали.prefetchInfiniteQueryawait queryClient.prefetchInfiniteQuery({
...feedQuery(userId),
initialPageParam: null,
});
pageParams в dehydrate, иначе клиент не узнает, какие курсоры уже загружены.dehydrate автоматически кладёт pageParams, но только если запрос успел завершиться успешно. Если prefetch оборвался, проверьте query.state.fetchStatus.initialPageParam в useInfiniteQuery — иначе React Query решит, что данных нет, и запустит новый запрос.prefetchInfiniteQuery в IntersectionObserver, как только пользователь добирается до конца.