Механизм бесконечных запросов в TanStack Query предназначен для работы с постраничной загрузкой данных, где количество элементов заранее неизвестно или потенциально велико. Такой подход применяется в лентах новостей, списках сообщений, комментариях, поисковой выдаче и любых интерфейсах, где данные подгружаются по мере прокрутки или по требованию пользователя.
В отличие от классической пагинации, где каждая страница запрашивается отдельно и управляется вручную, infinite queries объединяют страницы в единый логический поток данных, автоматически управляя состоянием загрузки, кэшированием и переходами между страницами.
Основной хук для работы с бесконечными запросами —
useInfiniteQuery. Его ключевая особенность заключается в
наличии параметра pageParam, который динамически изменяется
при каждой новой загрузке страницы.
Каждый запрос возвращает не просто массив данных, а структуру, содержащую набор страниц:
pages — массив загруженных страницpageParams — массив параметров, использованных для
получения каждой страницыТиповая структура результата:
{
pages: [
/* первая страница */,
/* вторая страница */
],
pageParams: [
/* параметры запросов */
]
}
Базовая конфигурация включает queryKey, функцию запроса
и функцию определения следующей страницы.
import { useInfiniteQuery } from '@tanstack/react-query'
const fetchPosts = async ({ pageParam = 0 }) => {
const res = await fetch(`/api/posts?cursor=${pageParam}`)
return res.json()
}
const query = useInfiniteQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor
})
pageParam — ключевой элемент infinite queries. Он
передаётся в queryFn и определяет, какую часть данных
необходимо загрузить.
На серверной стороне чаще всего используются два подхода:
Сервер возвращает nextCursor, который используется для
следующего запроса.
getNextPageParam: (lastPage) => lastPage.nextCursor
Если nextCursor отсутствует, TanStack Query понимает,
что данные закончились.
Используется смещение:
const fetchItems = async ({ pageParam = 0 }) => {
const res = await fetch(`/api/items?offset=${pageParam}&limit=20`)
return res.json()
}
getNextPageParam: (lastPage, allPages) => {
const nextOffset = allPages.length * 20
return nextOffset < lastPage.total ? nextOffset : undefined
}
Функция getNextPageParam определяет, существует ли
следующая страница. Если возвращается undefined, библиотека
прекращает дальнейшие запросы.
getNextPageParam: (lastPage) => {
if (!lastPage.hasMore) return undefined
return lastPage.nextCursor
}
Также доступна логика для предыдущих страниц:
getPreviousPageParam: (firstPage) => firstPage.prevCursor
Основные методы управления бесконечным списком:
Запрашивает следующую страницу данных.
query.fetchNextPage()
Используется реже, но поддерживается:
query.fetchPreviousPage()
Хук предоставляет расширенный набор состояний:
isFetchingNextPage — идёт загрузка следующей
страницыisFetchingPreviousPage — загрузка предыдущей
страницыhasNextPage — наличие следующей страницыhasPreviousPage — наличие предыдущей страницыisLoading — начальная загрузкаisError — ошибка запросаВсе полученные страницы объединяются в массив pages. Это
позволяет работать с данными как с единым списком:
const allItems = query.data.pages.flatMap(page => page.items)
Такой подход удобен для рендеринга списков без необходимости вручную объединять страницы.
Типичный сценарий — отображение списка с кнопкой загрузки дополнительных данных.
return (
<div>
{query.data.pages.map((page, i) => (
<div key={i}>
{page.items.map(item => (
<div key={item.id}>{item.title}</div>
))}
</div>
))}
<button
onCl ick={() => query.fetchNextPage()}
disabled={!query.hasNextPage || query.isFetchingNextPage}
>
Загрузить ещё
</button>
</div>
)
Интеграция с прокруткой реализуется через Intersection Observer:
import { useEffect, useRef } from 'react'
const sentinelRef = useRef(null)
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && query.hasNextPage) {
query.fetchNextPage()
}
})
if (sentinelRef.current) {
observer.observe(sentinelRef.current)
}
return () => observer.disconnect()
}, [query.hasNextPage])
return (
<div>
{query.data?.pages.map(page =>
page.items.map(item => <div key={item.id}>{item.title}</div>)
)}
<div ref={sentinelRef} />
</div>
)
Infinite queries полностью интегрированы в кэш TanStack Query. Это означает:
Дополнительные параметры управления:
staleTime: 1000 * 60 * 5,
gcTime: 1000 * 60 * 30
При изменении данных на сервере используется инвалидирование:
queryClient.invalidateQueries({ queryKey: ['posts'] })
Для infinite queries это приводит к пересборке страниц при следующем обращении.
Также возможно принудительное обновление первой страницы:
query.refetch()
pageParam может быть не только числом или строкой, но и
объектом:
const fetchMessages = async ({ pageParam }) => {
const res = await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify({
cursor: pageParam?.cursor,
direction: pageParam?.direction
})
})
return res.json()
}
getNextPageParam: (lastPage) => ({
cursor: lastPage.nextCursor,
direction: 'forward'
})
Это позволяет реализовывать сложные навигационные модели, включая чаты и ленты с нелинейной навигацией.
Несколько независимых бесконечных запросов могут использоваться одновременно:
const posts = useInfiniteQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
initialPageParam: 0,
getNextPageParam: (last) => last.nextCursor
})
const comments = useInfiniteQuery({
queryKey: ['comments'],
queryFn: fetchComments,
initialPageParam: 0,
getNextPageParam: (last) => last.nextCursor
})
Каждый запрос изолирован в кэше благодаря уникальному
queryKey.
При работе с большим количеством страниц важны следующие аспекты:
select для трансформации данных без
лишних перерасчётовflatMapПример оптимизации:
useInfiniteQuery({
queryKey: ['feed'],
queryFn: fetchFeed,
initialPageParam: 0,
getNextPageParam: (last) => last.nextCursor,
select: (data) => ({
...data,
flatItems: data.pages.flatMap(p => p.items)
})
})
Сброс состояния выполняется через удаление кэша:
queryClient.removeQueries({ queryKey: ['posts'] })
Или через ручной reset в UI-логике, приводящий к повторной
инициализации initialPageParam.
getNextPageParam, из-за чего невозможна
подгрузкаpageParam, приводящий к дублированию
страницqueryKeyhasNextPage перед загрузкойНа практике infinite queries используются в нескольких устойчивых моделях:
Во всех случаях ключевым элементом остаётся корректное управление курсором и предсказуемость структуры ответа сервера.