Infinite queries и useInfiniteQuery

Механизм бесконечных запросов в TanStack Query предназначен для работы с постраничной загрузкой данных, где количество элементов заранее неизвестно или потенциально велико. Такой подход применяется в лентах новостей, списках сообщений, комментариях, поисковой выдаче и любых интерфейсах, где данные подгружаются по мере прокрутки или по требованию пользователя.

В отличие от классической пагинации, где каждая страница запрашивается отдельно и управляется вручную, infinite queries объединяют страницы в единый логический поток данных, автоматически управляя состоянием загрузки, кэшированием и переходами между страницами.


Базовая концепция useInfiniteQuery

Основной хук для работы с бесконечными запросами — useInfiniteQuery. Его ключевая особенность заключается в наличии параметра pageParam, который динамически изменяется при каждой новой загрузке страницы.

Каждый запрос возвращает не просто массив данных, а структуру, содержащую набор страниц:

  • pages — массив загруженных страниц
  • pageParams — массив параметров, использованных для получения каждой страницы

Типовая структура результата:

{
  pages: [
    /* первая страница */,
    /* вторая страница */
  ],
  pageParams: [
    /* параметры запросов */
  ]
}

Инициализация useInfiniteQuery

Базовая конфигурация включает 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 и механизм курсоров

pageParam — ключевой элемент infinite queries. Он передаётся в queryFn и определяет, какую часть данных необходимо загрузить.

На серверной стороне чаще всего используются два подхода:

Курсорная пагинация

Сервер возвращает nextCursor, который используется для следующего запроса.

getNextPageParam: (lastPage) => lastPage.nextCursor

Если nextCursor отсутствует, TanStack Query понимает, что данные закончились.

Offset-пагинация

Используется смещение:

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 и завершение загрузки

Функция getNextPageParam определяет, существует ли следующая страница. Если возвращается undefined, библиотека прекращает дальнейшие запросы.

getNextPageParam: (lastPage) => {
  if (!lastPage.hasMore) return undefined
  return lastPage.nextCursor
}

Также доступна логика для предыдущих страниц:

getPreviousPageParam: (firstPage) => firstPage.prevCursor

Методы управления загрузкой

Основные методы управления бесконечным списком:

fetchNextPage

Запрашивает следующую страницу данных.

query.fetchNextPage()

fetchPreviousPage

Используется реже, но поддерживается:

query.fetchPreviousPage()

Состояния infinite query

Хук предоставляет расширенный набор состояний:

  • isFetchingNextPage — идёт загрузка следующей страницы
  • isFetchingPreviousPage — загрузка предыдущей страницы
  • hasNextPage — наличие следующей страницы
  • hasPreviousPage — наличие предыдущей страницы
  • isLoading — начальная загрузка
  • isError — ошибка запроса

Структура данных pages

Все полученные страницы объединяются в массив pages. Это позволяет работать с данными как с единым списком:

const allItems = query.data.pages.flatMap(page => page.items)

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


React-рендеринг бесконечного списка

Типичный сценарий — отображение списка с кнопкой загрузки дополнительных данных.

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

Автоматическая подгрузка (infinite scroll)

Интеграция с прокруткой реализуется через 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

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'
})

Это позволяет реализовывать сложные навигационные модели, включая чаты и ленты с нелинейной навигацией.


Параллельные infinite queries

Несколько независимых бесконечных запросов могут использоваться одновременно:

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

Сброс состояния infinite queries

Сброс состояния выполняется через удаление кэша:

queryClient.removeQueries({ queryKey: ['posts'] })

Или через ручной reset в UI-логике, приводящий к повторной инициализации initialPageParam.


Типичные ошибки при работе с infinite queries

  • отсутствие getNextPageParam, из-за чего невозможна подгрузка
  • некорректный pageParam, приводящий к дублированию страниц
  • отсутствие стабилизации queryKey
  • смешивание offset и cursor логики
  • отсутствие проверки hasNextPage перед загрузкой

Архитектурные паттерны использования

На практике infinite queries используются в нескольких устойчивых моделях:

  • лента контента (социальные сети)
  • бесконечные списки товаров
  • чат с подгрузкой истории сообщений
  • поисковые выдачи с дозагрузкой результатов
  • логирование событий и аудит-ленты

Во всех случаях ключевым элементом остаётся корректное управление курсором и предсказуемость структуры ответа сервера.