Пагинация в RTK Query реализуется как один из ключевых механизмов работы с коллекциями данных, когда сервер возвращает не весь набор сущностей сразу, а разбивает его на страницы. Такой подход критичен для производительности, снижения нагрузки на сеть и оптимизации рендеринга интерфейса. RTK Query предоставляет несколько стратегий реализации пагинации, каждая из которых подходит под определённые сценарии API и архитектуры данных.
Наиболее распространённый способ — передача параметров страницы в
query endpoint. Обычно это page и limit, либо
offset и limit, в зависимости от API.
import { createApi, fetchBaseQuery } fr om '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getPosts: builder.query({
query: ({ page = 1, lim it = 10 }) => ({
url: 'posts',
params: { page, limit }
})
})
})
})
export const { useGetPostsQuery } = api
В этом варианте каждый вызов хука формирует отдельный запрос, а RTK Query кэширует результат по ключу аргументов. Это означает, что страницы автоматически изолируются:
page=1 — один кэш-ключpage=2 — другой кэш-ключКлючевой момент заключается в том, что RTK Query рассматривает аргументы как часть идентификатора запроса. Поэтому:
useGetPostsQuery({ page: 1, limit: 10 })
useGetPostsQuery({ page: 2, limit: 10 })
создают два независимых куска состояния в кэше.
Это удобно для классической пагинации с переключением страниц, но не подходит для бесконечной подгрузки без дополнительной логики объединения данных.
Некоторые API используют offset вместо
page. Это даёт более точный контроль над смещением.
getPosts: builder.query({
query: ({ offset = 0, limit = 10 }) => ({
url: 'posts',
params: { offset, limit }
})
})
= ( - 1)
Такая модель часто применяется в SQL-базах, где offset напрямую влияет на выборку строк.
RTK Query не агрегирует страницы автоматически, поэтому для бесконечной прокрутки требуется ручное объединение результатов.
Один из способов — использовать selectFromResult для
объединения кэша:
const useInfinitePosts = ({ limit }) => {
const result = useGetPostsQuery({ page: 1, limit })
const pages = result.data?.pages ?? []
return {
...result,
data: pages.flat()
}
}
Однако такой подход ограничен, так как не сохраняет все страницы автоматически.
Более устойчивый способ — использовать
serializeQueryArgs и merge внутри
createApi.
getPosts: builder.query({
query: ({ page = 1, limit = 10 }) => ({
url: 'posts',
params: { page, limit }
}),
serializeQueryArgs: ({ endpointName }) => {
return endpointName
},
merge: (currentCache, newItems, { arg }) => {
if (arg.page === 1) {
return newItems
}
currentCache.push(...newItems)
},
forceRefetch({ currentArg, previousArg }) {
return currentArg?.page !== previousArg?.page
}
})
mergeЭто позволяет реализовать бесконечную прокрутку без дублирования данных в кэше.
Функция serializeQueryArgs определяет, как RTK Query
группирует запросы в кэш.
По умолчанию:
serializeQueryArgs: ({ endpointName, queryArgs }) =>
`${endpointName}(${JSON.stringify(queryArgs)})`
Для пагинации это не всегда подходит, потому что каждая страница становится отдельным ключом.
serializeQueryArgs: ({ endpointName }) => endpointName
Такой подход объединяет все страницы в одну сущность.
Функция merge вызывается при каждом новом запросе, если
кэш уже существует.
merge: (currentCache, newItems) => {
currentCache.push(...newItems)
}
Важно понимать:
Пагинация требует точного контроля повторных запросов:
forceRefetch: ({ currentArg, previousArg }) => {
return currentArg?.page !== previousArg?.page
}
Это предотвращает ненужные запросы при повторном использовании тех же параметров.
При агрегации страниц часто возникает проблема повторов. Она решается на уровне merge:
merge: (currentCache, newItems) => {
const existingIds = new Set(currentCache.map(item => item.id))
newItems.forEach(item => {
if (!existingIds.has(item.id)) {
currentCache.push(item)
}
})
}
Это особенно важно при нестабильной серверной пагинации.
Альтернативой offset/page является cursor-based модель, где сервер возвращает курсор последнего элемента.
query: ({ cursor, limit }) => ({
url: 'posts',
params: { cursor, limit }
})
{
"items": [...],
"nextCursor": "abc123"
}
merge: (currentCache, newResponse) => {
currentCache.items.push(...newResponse.items)
currentCache.nextCursor = newResponse.nextCursor
}
Cursor-подход обеспечивает:
Пагинированные данные требуют аккуратной инвалидизации:
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: 'Post', id })),
{ type: 'Post', id: 'PARTIAL-LIST' }
]
: [{ type: 'Post', id: 'PARTIAL-LIST' }]
При мутациях можно инвалидировать:
invalidatesTags: [{ type: 'Post', id: 'PARTIAL-LIST' }]
Это позволяет обновлять все страницы одновременно.
RTK Query предоставляет состояние загрузки:
isFetching — идёт любой запросisLoading — первый запросisSuccess — данные полученыДля пагинации важно различать первичную загрузку и подгрузку страниц:
const { data, isFetching } = useGetPostsQuery({ page })
При бесконечной прокрутке обычно:
RTK Query автоматически избегает повторных запросов при одинаковых
аргументах, однако при пагинации это поведение может быть изменено через
refetchOnMountOrArgChange.
refetchOnMountOrArgChange: true
Использование этого параметра оправдано, когда данные часто меняются.
Существует три основных подхода:
Каждый из подходов требует разной настройки RTK Query, особенно в
части serializeQueryArgs, merge и
forceRefetch.
При добавлении фильтров важно учитывать сброс кэша:
query: ({ page, limit, filter }) => ({
url: 'posts',
params: { page, limit, filter }
})
Если используется единый кэш, фильтр должен входить в ключ сериализации:
serializeQueryArgs: ({ endpointName, queryArgs }) => {
return `${endpointName}-${queryArgs.filter}`
}
Иначе данные разных фильтров смешаются.
Пагинация в RTK Query не является встроенной абстракцией уровня UI, а реализуется через комбинацию:
Эта гибкость позволяет строить как простые постраничные списки, так и сложные системы бесконечной загрузки с серверной поддержкой cursor-based навигации.