Dev Highlights

Доки по разработке

This project is maintained by teniryte

9. Prefetch, Hydration и начальные данные

Предзагрузка и гидратация позволяют отдавать пользователю готовые данные до первого рендера и уменьшать «белые экраны».

Когда prefetch срабатывает лучше всего

prefetchQuery

await queryClient.prefetchQuery(articleQuery(slug));
await queryClient.prefetchQuery({
  queryKey: ['article', slug],
  queryFn: () => fetchArticle(slug, { signal }),
  staleTime: 5 * 60_000,
  gcTime: 30 * 60_000,
  meta: { source: 'hover' },
});

Prefetch в роутерах

// 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;
  };

dehydrate / Hydrate

// server.tsx
await queryClient.prefetchQuery(articleQuery(slug));
const dehydratedState = dehydrate(queryClient);

// client.tsx
<Hydrate state={dehydratedState}>
  <App />
</Hydrate>
const dehydratedState = dehydrate(queryClient, {
  shouldDehydrateQuery: query =>
    query.state.status === 'success' &&
    query.queryKey[0] !== 'debug-only',
});
<HydrationBoundary state={headerState}>
  <Header />
</HydrationBoundary>
<HydrationBoundary state={contentState}>
  <Article />
</HydrationBoundary>

Next.js / Remix

// 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>
  );
}

initialData vs placeholderData

useQuery({
  ...articleQuery(slug),
  initialData: () => queryClient.getQueryData(['articles-list'])?.find(a => a.slug === slug),
  initialDataUpdatedAt: () => queryClient.getQueryState(['articles-list'])?.dataUpdatedAt,
});

Таким образом дата обновления совпадает с исходной записью, и staleTime учитывается корректно.

ensureQueryData

const article = await queryClient.ensureQueryData(articleQuery(slug));
await Promise.all([
  queryClient.ensureQueryData(profileQuery(userId)),
  queryClient.ensureQueryData(recommendationsQuery(userId)),
]);

Prefetch при наведении

const prefetchArticle = (slug: string) => {
  queryClient.prefetchQuery(articlePreviewQuery(slug), {
    staleTime: 60_000,
  });
};

<Link onMouseEnter={() => prefetchArticle(article.slug)} />

Пользователь получит мгновенный рендер карточки.

Prefetch по IntersectionObserver

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);

Работа с placeholderData

useQuery({
  ...productQuery(id),
  placeholderData: previousData => previousData ?? skeletonProduct,
});

prefetchInfiniteQuery

await queryClient.prefetchInfiniteQuery({
  ...feedQuery(userId),
  initialPageParam: null,
});