Pagination

Базовая модель серверной пагинации

Пагинация в TanStack Query строится вокруг идеи управляемого запроса с параметрами страницы. Основная форма — классическая offset-based пагинация, где данные запрашиваются частями через параметры page и limit (или offset и limit).

Ключевой принцип заключается в том, что каждый набор данных должен иметь уникальный queryKey, включающий параметры страницы.

import { useQuery } fr om '@tanstack/react-query'

function useUsers(page, lim it) {
  return useQuery({
    queryKey: ['users', page, limit],
    queryFn: async () => {
      const res = await fetch(`/api/users?page=${page}&limit=${limit}`)
      return res.json()
    }
  })
}

Изменение page автоматически приводит к созданию нового запроса и сохранению результатов в кеше отдельно от других страниц.


Структура queryKey для пагинации

Корректная структура ключей определяет поведение кеша. Любое значение, влияющее на результат запроса, должно быть частью ключа.

Типовые варианты:

['users', page]
['users', { page, limit }]
['users', { page, limit, sort, filter }]

Использование объектов в ключах повышает читаемость и масштабируемость, но требует стабильности ссылок или сериализации.

Важный принцип: кеш TanStack Query работает на строгом сравнении ключей, поэтому изменение даже одного параметра создаёт новый кеш-запрос.


Сохранение предыдущих данных при переключении страниц

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

keepPreviousData (v4)

import { useQuery, keepPreviousData } from '@tanstack/react-query'

function useUsers(page) {
  return useQuery({
    queryKey: ['users', page],
    queryFn: () => fetch(`/api/users?page=${page}`).then(r => r.json()),
    placeholderData: keepPreviousData
  })
}

Поведение:

  • данные предыдущей страницы остаются в кеше активными
  • UI не очищается при переходе между страницами
  • новый запрос обновляет данные асинхронно

placeholderData (v5 подход)

В новых версиях используется более универсальный механизм:

placeholderData: (previousData) => previousData

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


Управление кешем страниц

Каждая страница существует в кеше как отдельный query. Это приводит к росту памяти при большом количестве страниц.

Основные стратегии управления:

  1. Ограничение времени жизни кеша
useQuery({
  queryKey: ['users', page],
  queryFn: fetchUsers,
  gcTime: 1000 * 60 * 5
})
  1. Уменьшение staleTime для контроля актуальности
staleTime: 1000 * 30
  1. Явная очистка кеша
queryClient.removeQueries({
  queryKey: ['users']
})

Offset-based пагинация

Наиболее распространённый вариант — использование page и limit.

Серверная реализация

GET /api/users?page=2&limit=20

Клиентская логика

const [page, setPage] = useState(1)

const { data, isLoading } = useQuery({
  queryKey: ['users', page],
  queryFn: () =>
    fetch(`/api/users?page=${page}&limit=20`).then(r => r.json())
})

Переключение страниц

<button onCl ick={() => setPage(p => p - 1)}>Prev</button>
<button onCl ick={() => setPage(p => p + 1)}>Next</button>

Cursor-based пагинация

Cursor-подход используется при больших объёмах данных и динамическом наборе.

Принцип работы

Вместо номера страницы используется курсор — значение последнего элемента.

GET /api/users?cursor=abc123&limit=20

Реализация через useInfiniteQuery

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

function useUsers() {
  return useInfiniteQuery({
    queryKey: ['users'],
    queryFn: ({ pageParam }) =>
      fetch(`/api/users?cursor=${pageParam ?? ''}`).then(r => r.json()),
    getNextPageParam: (lastPage) => lastPage.nextCursor
  })
}

Объединение страниц в единый список

useInfiniteQuery возвращает структуру страниц, которую часто нужно нормализовать.

const users = data.pages.flatMap(page => page.items)

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


Предзагрузка следующей страницы

Для улучшения UX применяется prefetch.

queryClient.prefetchQuery({
  queryKey: ['users', nextPage],
  queryFn: () =>
    fetch(`/api/users?page=${nextPage}`).then(r => r.json())
})

Предзагрузка снижает задержку при переходе между страницами и делает интерфейс более отзывчивым.


Оптимизация переключения страниц

1. Стабильные queryKey

Любые нестабильные значения в ключе приводят к лишним запросам.

['users', { page }] // плохо, если объект создаётся каждый рендер без мемоизации

Решение:

['users', page] // предпочтительно

2. Использование select для трансформации данных

useQuery({
  queryKey: ['users', page],
  queryFn: fetchUsers,
  select: (data) => data.items.map(u => u.name)
})

Позволяет минимизировать лишние перерасчёты UI.


3. Кеширование страниц

TanStack Query хранит каждую страницу отдельно, поэтому важно контролировать:

  • gcTime
  • staleTime
  • количество уникальных страниц

Синхронизация пагинации с URL

Частый сценарий — хранение страницы в query-параметрах URL.

const page = Number(searchParams.get('page') ?? 1)

Изменение страницы:

setSearchParams({ page: String(newPage) })

Такой подход позволяет:

  • сохранять состояние навигации
  • поддерживать глубокие ссылки
  • использовать браузерную историю

Ошибки и состояния загрузки

При пагинации важно учитывать различие состояний:

const {
  data,
  isLoading,
  isFetching,
  isError
} = useQuery(...)
  • isLoading — первый запрос
  • isFetching — любые последующие обновления
  • isError — ошибка запроса

Комбинация этих флагов определяет UX переходов между страницами.


Гибридная пагинация (offset + cursor)

В некоторых API используется смешанный подход:

  • offset для страниц
  • cursor для точной позиции

TanStack Query поддерживает это через составной queryKey:

['users', { page, cursor }]

и соответствующую логику queryFn.


Инвалидация страниц

При изменении данных необходимо обновлять кеш всех страниц.

queryClient.invalidateQueries({
  queryKey: ['users']
})

Это приводит к перезапросу всех страниц, связанных с ключом users.


Типичные архитектурные ошибки

  • отсутствие page в queryKey
  • хранение данных пагинации вне TanStack Query
  • смешивание разных типов пагинации в одном ключе
  • отсутствие контроля кеша при большом количестве страниц
  • попытки вручную синхронизировать состояние вместо использования invalidateQueries

Масштабирование пагинации

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

Используются следующие подходы:

  • переход на cursor pagination
  • ограничение количества закешированных страниц
  • использование infinite queries вместо page-based логики
  • предзагрузка ближайших страниц
  • нормализация данных вне TanStack Query при сложных структурах