Dev Highlights

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

This project is maintained by teniryte

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

Эта глава отвечает на главный production-вопрос: как убрать client-side waterfall и отдать пользователю готовые данные как можно раньше.

prefetchQuery

await queryClient.prefetchQuery(articleOptions(slug))

prefetchQuery:

Если данные уже свежие, вызов фактически ничего не делает.

ensureQueryData

const article = await queryClient.ensureQueryData(articleOptions(slug))

Это один из самых полезных API:

Prefetch по hover/focus

function ArticleLink({ slug, children }: { slug: string; children: React.ReactNode }) {
  const queryClient = useQueryClient()

  const prefetch = () => {
    void queryClient.prefetchQuery(articleOptions(slug))
  }

  return (
    <Link href={`/articles/${slug}`} onPointerEnter={prefetch} onFocus={prefetch}>
      {children}
    </Link>
  )
}

Это хороший паттерн для вероятных переходов.

usePrefetchQuery

Актуальный API для prefetch во время render перед Suspense boundary:

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

function ArticleRoute({ slug }: { slug: string }) {
  usePrefetchQuery(articleOptions(slug))

  return (
    <Suspense fallback={<ArticleSkeleton />}>
      <ArticleScreen slug={slug} />
    </Suspense>
  )
}

Особенности:

dehydrate и HydrationBoundary

Современный SSR-паттерн:

import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query'

export default async function Page({ params }: { params: { slug: string } }) {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery(articleOptions(params.slug))

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <ArticleClient slug={params.slug} />
    </HydrationBoundary>
  )
}

На клиенте:

'use client'

function ArticleClient({ slug }: { slug: string }) {
  const query = useQuery(articleOptions(slug))
  return <ArticleView article={query.data} />
}

HydrationBoundary является современным рекомендуемым способом гидратации в React-ветке документации.

initialData

const postQuery = useQuery({
  ...postOptions(postId),
  initialData: () =>
    queryClient
      .getQueryData<PostPreview[]>(['posts', 'list'])
      ?.find((post) => post.id === postId),
  initialDataUpdatedAt: () =>
    queryClient.getQueryState(['posts', 'list'])?.dataUpdatedAt,
})

Хороший сценарий:

placeholderData

Для переходов между ключами, страницами и фильтрами:

useQuery({
  ...postsOptions(filters),
  placeholderData: (previousData) => previousData,
})

Разница с initialData:

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

Через dehydrate можно фильтровать:

const dehydratedState = dehydrate(queryClient, {
  shouldDehydrateQuery: (query) =>
    query.state.status === 'success' &&
    query.queryKey[0] !== 'admin-debug',
})

Не стоит сериализовать:

Prefetch нескольких блоков

await Promise.all([
  queryClient.ensureQueryData(profileOptions(userId)),
  queryClient.ensureQueryData(recommendationsOptions(userId)),
  queryClient.ensureQueryData(notificationsOptions(userId)),
])

Это стандартный способ убрать waterfall между секциями.

Infinite query prefetch

await queryClient.prefetchInfiniteQuery({
  queryKey: ['feed'],
  initialPageParam: null,
  queryFn: ({ pageParam }) => fetchFeed(pageParam),
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

Важно:

Практические рекомендации