Prefetching — механизм предварительной загрузки данных в кеш до момента, когда компоненту реально понадобятся эти данные. Главная цель — сократить задержки интерфейса и убрать эффект «пустого состояния» при переходе между страницами, открытии модальных окон, вкладок и сложных интерфейсных блоков.
Вместо ожидания загрузки после рендера компонента приложение может
заранее поместить данные в кеш. Когда компонент вызовет
useQuery, данные уже будут находиться в памяти
QueryClient.
Наиболее частые сценарии:
TanStack Query хранит данные внутри QueryCache.
Prefetching выполняет запрос заранее и записывает результат в кеш под
указанным queryKey.
Когда позже выполняется:
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
библиотека сначала проверяет наличие данных в кеше.
Если данные уже существуют и не считаются устаревшими:
isLoading может вообще не появиться.Основной API для prefetching:
queryClient.prefetchQuery()
Пример:
import { useQueryClient } from '@tanstack/react-query'
function App() {
const queryClient = useQueryClient()
const prefetchPosts = async () => {
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
}
return (
<button onMouseEn ter={prefetchPosts}>
Открыть статьи
</button>
)
}
После наведения курсора данные загружаются в кеш.
При переходе на страницу:
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
данные уже будут доступны.
Многие разработчики путают эти методы.
await queryClient.prefetchQuery(...)
Особенности:
const data = await queryClient.fetchQuery(...)
Особенности:
Предварительно загруженные данные всё равно подчиняются правилам устаревания.
Пример:
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 1000 * 60
})
Если в течение минуты вызвать useQuery с тем же
queryKey, новый запрос не будет выполнен.
Без staleTime данные почти сразу считаются устаревшими,
поэтому useQuery может инициировать повторный refetch.
Один из самых популярных паттернов.
function PostLink({ id }) {
const queryClient = useQueryClient()
const prefetchPost = () => {
queryClient.prefetchQuery({
queryKey: ['post', id],
queryFn: () => fetchPost(id),
staleTime: 1000 * 60 * 5
})
}
return (
<Link
to={`/posts/${id}`}
onMouseEn ter={prefetchPost}
>
Статья
</Link>
)
}
Пока пользователь двигает курсор к ссылке, запрос уже начинает выполняться.
В результате:
Очень важный сценарий для UX.
const { data } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page)
})
После получения текущей страницы можно прогреть следующую:
useEffect(() => {
if (data?.hasMore) {
queryClient.prefetchQuery({
queryKey: ['posts', page + 1],
queryFn: () => fetchPosts(page + 1)
})
}
}, [data, page, queryClient])
Когда пользователь нажимает «Следующая страница», данные уже находятся в кеше.
Частая проблема интерфейсов — задержка при открытии модалки.
Prefetching решает проблему заранее.
function UserRow({ user }) {
const queryClient = useQueryClient()
const prefetchUser = () => {
queryClient.prefetchQuery({
queryKey: ['user', user.id],
queryFn: () => fetchUser(user.id)
})
}
return (
<div
onMouseEn ter={prefetchUser}
>
{user.name}
</div>
)
}
После открытия модального окна:
const { data } = useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id)
})
информация отображается мгновенно.
Современные SPA часто загружают данные во время перехода между страницами.
Пример с роутером:
const openProfile = async (id) => {
await queryClient.prefetchQuery({
queryKey: ['profile', id],
queryFn: () => fetchProfile(id)
})
navigate(`/profile/${id}`)
}
Маршрут открывается уже с готовыми данными.
TanStack Query хорошо интегрируется с loaders.
export const profileLoader =
(queryClient) =>
async ({ params }) => {
await queryClient.prefetchQuery({
queryKey: ['profile', params.id],
queryFn: () => fetchProfile(params.id)
})
return null
}
Компонент:
function ProfilePage() {
const { data } = useQuery({
queryKey: ['profile'],
queryFn: fetchProfile
})
return <div>{data.name}</div>
}
Данные уже готовы к моменту рендера.
Для infinite-запросов используется отдельный API.
queryClient.prefetchInfiniteQuery()
Пример:
await queryClient.prefetchInfiniteQuery({
queryKey: ['feed'],
queryFn: fetchFeed,
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor
})
Такой prefetching полезен для:
На сервере prefetching особенно важен.
Типичный поток:
Пример:
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
После этого:
const dehydratedState = dehydrate(queryClient)
На клиенте:
<Hydrate state={dehydratedState}>
<App />
</Hydrate>
Это основа SSR в TanStack Query.
Hydration восстанавливает кеш, созданный на сервере.
Prefetching здесь играет ключевую роль:
Без hydration браузер выполнил бы запрос повторно.
Иногда prefetching называют cache warming — прогрев кеша.
Пример после авторизации:
await Promise.all([
queryClient.prefetchQuery({
queryKey: ['profile'],
queryFn: fetchProfile
}),
queryClient.prefetchQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications
}),
queryClient.prefetchQuery({
queryKey: ['messages'],
queryFn: fetchMessages
})
])
После входа интерфейс становится значительно быстрее.
TanStack Query не ограничивает количество prefetch-запросов.
Пример:
await Promise.all([
queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts
}),
queryClient.prefetchQuery({
queryKey: ['users'],
queryFn: fetchUsers
}),
queryClient.prefetchQuery({
queryKey: ['settings'],
queryFn: fetchSettings
})
])
Такой подход полезен для dashboard-интерфейсов.
Важно понимать разницу.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
initialData: []
})
await queryClient.prefetchQuery(...)
placeholderData создаёт временное отображение до прихода
настоящих данных.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
placeholderData: []
})
Prefetching отличается тем, что реальные данные уже существуют к моменту рендера.
Ошибки не отображаются автоматически в интерфейсе.
Пример:
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
Если запрос завершится неудачно:
useQuery позже выполнит обычный запрос;Для обработки:
try {
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
} catch (error) {
console.error(error)
}
Prefetching способен радикально улучшить UX, но при неправильном использовании создаёт проблемы:
Переходы становятся мгновенными.
Множество параллельных данных можно загрузить заранее.
Следующая страница уже находится в кеше.
Prefetching скрывает задержки сети.
Позволяет избежать двойных запросов.
Не стоит заранее загружать мегабайты информации.
Если пользователь редко открывает раздел, prefetching только расходует ресурсы.
Предварительные запросы могут ухудшить UX.
Если данные устаревают за секунды, prefetching теряет смысл.
После prefetch данные остаются в кеше до garbage collection.
Пример:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 10
}
}
})
Через 10 минут неиспользуемый кеш будет удалён.
После инвалидирования prefetch-данные могут стать устаревшими.
queryClient.invalidateQueries({
queryKey: ['posts']
})
Следующий useQuery выполнит refetch.
Даже если данные были prefetched, TanStack Query может обновить их в фоне.
Например:
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
refetchOnWindowFocus: true
})
После возврата во вкладку произойдёт background refetch.
Наиболее эффективен prefetch с высокой вероятностью использования.
Без staleTime prefetch часто теряет смысл из-за
мгновенного refetch.
Не стоит prefetch-загружать всё приложение сразу.
Лучшие триггеры:
function ProductCard({ id }) {
const queryClient = useQueryClient()
const prefetchProduct = () => {
queryClient.prefetchQuery({
queryKey: ['product', id],
queryFn: () => fetchProduct(id),
staleTime: 1000 * 60 * 5
})
}
return (
<Link
to={`/products/${id}`}
onMouseEn ter={prefetchProduct}
>
Открыть товар
</Link>
)
}
После открытия страницы:
function ProductPage({ id }) {
const { data } = useQuery({
queryKey: ['product', id],
queryFn: () => fetchProduct(id)
})
return <div>{data.title}</div>
}
Интерфейс получает данные практически мгновенно благодаря заранее прогретому кешу.