Пагинация в 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 автоматически приводит к созданию нового
запроса и сохранению результатов в кеше отдельно от других страниц.
Корректная структура ключей определяет поведение кеша. Любое значение, влияющее на результат запроса, должно быть частью ключа.
Типовые варианты:
['users', page]
['users', { page, limit }]
['users', { page, limit, sort, filter }]
Использование объектов в ключах повышает читаемость и масштабируемость, но требует стабильности ссылок или сериализации.
Важный принцип: кеш TanStack Query работает на строгом сравнении ключей, поэтому изменение даже одного параметра создаёт новый кеш-запрос.
При переключении страниц часто возникает эффект «мигания» интерфейса. TanStack Query предоставляет механизмы, позволяющие сохранить предыдущие данные до загрузки новых.
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
})
}
Поведение:
В новых версиях используется более универсальный механизм:
placeholderData: (previousData) => previousData
Это позволяет полностью контролировать стратегию отображения промежуточных данных.
Каждая страница существует в кеше как отдельный query. Это приводит к росту памяти при большом количестве страниц.
useQuery({
queryKey: ['users', page],
queryFn: fetchUsers,
gcTime: 1000 * 60 * 5
})
staleTime: 1000 * 30
queryClient.removeQueries({
queryKey: ['users']
})
Наиболее распространённый вариант — использование 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-подход используется при больших объёмах данных и динамическом наборе.
Вместо номера страницы используется курсор — значение последнего элемента.
GET /api/users?cursor=abc123&limit=20
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())
})
Предзагрузка снижает задержку при переходе между страницами и делает интерфейс более отзывчивым.
Любые нестабильные значения в ключе приводят к лишним запросам.
['users', { page }] // плохо, если объект создаётся каждый рендер без мемоизации
Решение:
['users', page] // предпочтительно
useQuery({
queryKey: ['users', page],
queryFn: fetchUsers,
select: (data) => data.items.map(u => u.name)
})
Позволяет минимизировать лишние перерасчёты UI.
TanStack Query хранит каждую страницу отдельно, поэтому важно контролировать:
gcTimestaleTimeЧастый сценарий — хранение страницы в query-параметрах URL.
const page = Number(searchParams.get('page') ?? 1)
Изменение страницы:
setSearchParams({ page: String(newPage) })
Такой подход позволяет:
При пагинации важно учитывать различие состояний:
const {
data,
isLoading,
isFetching,
isError
} = useQuery(...)
isLoading — первый запросisFetching — любые последующие обновленияisError — ошибка запросаКомбинация этих флагов определяет UX переходов между страницами.
В некоторых API используется смешанный подход:
TanStack Query поддерживает это через составной
queryKey:
['users', { page, cursor }]
и соответствующую логику queryFn.
При изменении данных необходимо обновлять кеш всех страниц.
queryClient.invalidateQueries({
queryKey: ['users']
})
Это приводит к перезапросу всех страниц, связанных с ключом
users.
При росте данных основной проблемой становится не скорость запроса, а управление кешем.
Используются следующие подходы: