Infinite Query endpoint definition
Infinite query endpoints (build.infiniteQuery()) are used to cache multi-page data sets from the server. They have all the same callbacks and options as standard query endpoints, but also require an infiniteQueryOptions field to specify how to calculate unique parameters to fetch each page. For infinite queries, there is separation between the query arg used for the cache key and the page param used to fetch a specific page. The query and queryFn methods receive a combined {queryArg, pageParam} object as the argument.
useInfiniteQuery hook signature and parameters
useInfiniteQuery accepts arg (query argument or skipToken) and options (UseInfiniteQueryOptions). UseInfiniteQueryOptions include pollingInterval, skipPollingIfUnfocused, refetchOnReconnect, refetchOnFocus, skip, refetchOnMountOrArgChange, selectFromResult, initialPageParam, and refetchCachedPages. Returns UseInfiniteQueryResult.
useInfiniteQuery return object properties
UseInfiniteQueryResult<Data, PageParam> contains: originalArgs, data (InfiniteData with pages array and pageParams array), currentData, error, requestId, endpointName, startedTimeStamp, fulfilledTimeStamp, isUninitialized, isLoading, isFetching, isSuccess, isError, hasNextPage (another page available querying forwards), hasPreviousPage (another page available querying backwards), isFetchingNextPage, isFetchingPreviousPage, isFetchNextPageError, isFetchPreviousPageError, refetch (function accepting optional refetchCachedPages), fetchNextPage (triggers next page fetch), and fetchPreviousPage (triggers previous page fetch).
InfiniteData structure
InfiniteData<Data, PageParam> is an object with two properties: pages (array of Data) and pageParams (array of PageParam). This structure holds accumulated pages and their corresponding page parameters for infinite queries.
useInfiniteQueryState hook purpose
useInfiniteQueryState is an implementation hook that returns the state for an infinite query without automatically triggering requests or subscribing the component to updates.
useInfiniteQuerySubscription hook purpose
useInfiniteQuerySubscription is an implementation hook that subscribes a component to infinite query state and manages fetching behavior, without returning full query state.
RTK Query supports infinite scrolling
RTK Query supports infinite scrolling through the infiniteQuery endpoint type.
Infinite queries overview and use case
Infinite Query endpoints in RTK Query support rendering lists that additively load more data or infinite scroll. They are similar to standard query endpoints in that they fetch data and cache results, but they have the ability to fetch next and previous pages, and contain all related fetched pages in a single cache entry.
Query arg vs page param separation in infinite queries
In infinite queries, there is a separation between the query arg (used to generate the unique cache key) and the page param (used to fetch a specific page). The query and queryFn methods receive a combined object {queryArg, pageParam} as the first argument, instead of just the queryArg by itself.
Infinite query cache entry structure
The data field in an infinite query cache entry stores a {pages: DataType[], pageParams: PageParam[]} structure that contains all fetched page results and their corresponding page params used to fetch them.
Infinite query definition with build.infiniteQuery()
Infinite query endpoints are defined by returning an object inside the endpoints section of createApi using the build.infiniteQuery() method. They are an extension of standard query endpoints and can specify the same options as standard queries (providing query or queryFn, customizing with transformResponse, lifecycles with onCacheEntryAdded and onQueryStarted, defining tags, etc). They also require an additional infiniteQueryOptions field to specify the infinite query behavior.
infiniteQuery generics for TypeScript
With TypeScript, you must supply 3 generic arguments to build.infiniteQuery<ResultType, QueryArg, PageParam>, where ResultType is the contents of a single page, QueryArg is the type passed in as the cache key, and PageParam is the value used to request a specific page. If there is no argument, use void for the arg type instead.
infiniteQueryOptions required and optional fields
The infiniteQueryOptions field includes: initialPageParam (the default page param value used for the first request, if not specified at usage site) - required, maxPages (optional limit to how many fetched pages will be kept in cache entry at a time), getNextPageParam (required callback to calculate next page param given existing cached pages and page params), getPreviousPageParam (optional callback to calculate previous page param for backwards fetching). Both initialPageParam and getNextPageParam are required to ensure the infinite query can properly fetch the next page of data.
PageParamFunction signature
The PageParamFunction type signature is: (currentPage: DataType, allPages: DataType[], currentPageParam: PageParam, allPageParams: PageParam[], queryArg: QueryArg) => PageParam | undefined | null. A page param can be any value: numbers, strings, objects, arrays, etc. The 'current' arguments will be either the last page for getNextPageParam, or the first page for getPreviousPageParam. If there is no possible page to fetch in that direction, the callback should return undefined.
Automatic hook generation for infinite query endpoints
RTK Query automatically generates React hooks for infinite query endpoints based on the endpoint name. An endpoint field like getPokemon: build.infiniteQuery() will generate a hook named useGetPokemonInfiniteQuery, as well as a generically-named hook attached to the endpoint like api.endpoints.getPokemon.useInfiniteQuery.
Three types of infinite query hooks
There are 3 infinite query-related hooks: 1) useInfiniteQuery - composes useInfiniteQuerySubscription and useInfiniteQueryState, is the primary hook, automatically triggers fetches and subscribes the component to cached data, and reads request status and cached data from Redux store. 2) useInfiniteQuerySubscription - returns refetch function and fetchNext/PreviousPage functions, accepts all hooks options, automatically triggers refetches and subscribes to cached data. 3) useInfiniteQueryState - returns query state and accepts skip and selectFromResult, reads request status and cached data from Redux store.
Infinite query hook parameters
The infinite query hooks expect two parameters: (queryArg?, queryOptions?). The queryOptions object accepts all the same parameters as useQuery, including skip, selectFromResult, and refetching/polling options. Unlike normal query hooks, the query or queryFn callbacks receive a page param value to generate the URL or make the request, instead of the query arg. By default, the initialPageParam value specified in the endpoint is used for the first request, then getNext/PreviousPageParam callbacks calculate further page params.
Override initialPageParam at hook usage
To start from a different page param than the endpoint default, you can override the initialPageParam by passing it as part of the hook options: const { data } = useGetPokemonInfiniteQuery('fire', { initialPageParam: 3 }). The next and previous page params will still be calculated as needed.
Infinite query hook return values
Infinite query hooks return the same result object as normal query hooks, but with additional fields specific to infinite queries and a different structure for data and currentData. data/currentData contain the {pages, pageParams} infinite query object with all fetched pages. hasNextPage/hasPreviousPage indicate when another page should be available to fetch in that direction. isFetchingNext/PreviousPage indicate when the isFetching flag represents a fetch in that direction. isFetchNext/PreviousPageError indicate when the isError flag represents an error for a failed fetch in that direction. fetchNext/PreviousPage are methods that trigger a fetch for another page in that direction.
Overlapping page fetches not possible
Since all pages are stored in a single cache entry, there can only be one request in progress at a time. RTK Query has logic built in to bail out of running a new request if there is already a request in flight for that cache entry. If you call fetchNextPage() while an existing request is in progress, the second call won't actually execute a request. Check the isFetching flag or await the previous fetchNextPage() promise first. The promise returned from fetchNextPage() has a promise.abort() method attached that will force the earlier request to reject and not save the results, which marks the cache entry as errored but data still exists.
Infinite query refetching behavior
When an infinite query endpoint is refetched (due to tag invalidation, polling, arg change, or manual refetching), RTK Query's default behavior is sequentially refetching all pages currently in the cache to ensure the client works with latest data and avoids stale cursors or duplicate records. If the cache entry is removed and re-added, it starts with only fetching the initial page. The refetchCachedPages option can override this to only refetch the first page, shrinking the cache from N pages to 1 page. It can be defined on the endpoint as part of infiniteQueryOptions, passed as an option to useInfiniteQuery hook, or passed as an option to endpoint.initiate() or refetch method.
maxPages option for limiting cache entry size
By default, there is no limit to the number of stored pages in the pages array. If you need to limit the number of stored pages for memory usage reasons, supply a maxPages option as part of the endpoint. If provided, fetching a page when already at the max will automatically drop the last page in the opposite direction. For example, with maxPages: 3 and cached page params of [1, 2, 3], calling fetchNextPage() would result in page 1 being dropped and cached pages becoming [2, 3, 4].
Schema validation for infinite query endpoints
Endpoints can use Standard Schema compliant libraries for runtime validation of query args, responses, and errors. Most commonly, you'll want to use responseSchema to validate the server response, or rawResponseSchema when using transformResponse. With TypeScript, schemas can be used to infer the type of that value instead of having to declare it manually.
Infinite query definition example with basic pagination
Example of infinite query endpoint with basic pagination:
```ts
type Pokemon = {
id: string
name: string
}
const pokemonApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com/pokemon' }),
endpoints: (build) => ({
getInfinitePokemonWithMax: build.infiniteQuery<Pokemon[], string, number>({
infiniteQueryOptions: {
initialPageParam: 1,
maxPages: 3,
getNextPageParam: (
lastPage,
allPages,
lastPageParam,
allPageParams,
queryArg,
) => lastPageParam + 1,
getPreviousPageParam: (
firstPage,
allPages,
firstPageParam,
allPageParams,
queryArg,
) => {
return firstPageParam > 0 ? firstPageParam - 1 : undefined
},
},
query({ queryArg, pageParam }) {
return `/type/${queryArg}?page=${pageParam}`
},
}),
}),
})
```
Infinite query hook usage example with component
Example of infinite query endpoint definition and hook usage:
```tsx
type Pokemon = {
id: string
name: string
}
const pokemonApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com/pokemon' }),
endpoints: (build) => ({
getPokemon: build.infiniteQuery<Pokemon[], string, number>({
infiniteQueryOptions: {
initialPageParam: 1,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
lastPageParam + 1,
},
query({ queryArg, pageParam }) {
return `/type/${queryArg}?page=${pageParam}`
},
}),
}),
})
function PokemonList({ pokemonType }: { pokemonType: string }) {
const { data, isFetching, fetchNextPage, fetchPreviousPage, refetch } =
pokemonApi.useGetPokemonInfiniteQuery(pokemonType)
const handleNextPage = async () => {
await fetchNextPage()
}
const handleRefetch = async () => {
await refetch()
}
const allResults = data?.pages.flat() ?? []
return (
<div>
<div>Type: {pokemonType}</div>
<div>
{allResults.map((pokemon, i: number | null | undefined) => (
<div key={i}>{pokemon.name}</div>
))}
</div>
<button onClick={() => handleNextPage()}>Fetch More</button>
<button onClick={() => handleRefetch()}>Refetch</button>
</div>
)
}
```
Basic pagination pattern example
Example of basic pagination with simple page numbers:
```ts
const pokemonApi = createApi({
baseQuery,
endpoints: (build) => ({
getInfinitePokemon: build.infiniteQuery<Pokemon[], string, number>({
infiniteQueryOptions: {
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam) =>
lastPageParam + 1,
getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
return firstPageParam > 0 ? firstPageParam - 1 : undefined
},
},
query({ pageParam }) {
return `https://example.com/listItems?page=${pageParam}`
},
}),
}),
})
```
Pagination with page size pattern example
Example of pagination with page number and size that uses totalPages from response:
```ts
type ProjectsResponse = {
projects: Project[]
serverTime: string
totalPages: number
}
type ProjectsInitialPageParam = {
page: number
size: number
}
const projectsApi = createApi({
baseQuery,
endpoints: (build) => ({
projectsPaginated: build.infiniteQuery<
ProjectsResponse,
void,
ProjectsInitialPageParam
>({
infiniteQueryOptions: {
initialPageParam: {
page: 0,
size: 20,
},
getNextPageParam: (
lastPage,
allPages,
lastPageParam,
allPageParams,
) => {
const nextPage = lastPageParam.page + 1
const remainingPages = lastPage?.totalPages - nextPage
if (remainingPages <= 0) {
return undefined
}
return {
...lastPageParam,
page: nextPage,
}
},
getPreviousPageParam: (
firstPage,
allPages,
firstPageParam,
allPageParams,
) => {
const prevPage = firstPageParam.page - 1
if (prevPage < 0) return undefined
return {
...firstPageParam,
page: prevPage,
}
},
},
query: ({ pageParam: { page, size } }) => {
return `https://example.com/api/projectsPaginated?page=${page}&size=${size}`
},
}),
}),
})
```
Bidirectional cursors pattern example
Example of bidirectional cursor-based pagination using server-provided cursor values:
```ts
type ProjectsCursorPaginated = {
projects: Project[]
serverTime: string
pageInfo: {
startCursor: number
endCursor: number
hasNextPage: boolean
hasPreviousPage: boolean
}
}
type ProjectsInitialPageParam = {
before?: number
around?: number
after?: number
limit: number
}
type QueryParamLimit = number
const projectsApi = createApi({
baseQuery,
endpoints: (build) => ({
getProjectsBidirectionalCursor: build.infiniteQuery<
ProjectsCursorPaginated,
QueryParamLimit,
ProjectsInitialPageParam
>({
infiniteQueryOptions: {
initialPageParam: { limit: 10 },
getPreviousPageParam: (
firstPage,
allPages,
firstPageParam,
allPageParams,
) => {
if (!firstPage.pageInfo.hasPreviousPage) {
return undefined
}
return {
before: firstPage.pageInfo.startCursor,
limit: firstPageParam.limit,
}
},
getNextPageParam: (
lastPage,
allPages,
lastPageParam,
allPageParams,
) => {
if (!lastPage.pageInfo.hasNextPage) {
return undefined
}
return {
after: lastPage.pageInfo.endCursor,
limit: lastPageParam.limit,
}
},
},
query: ({ pageParam: { before, after, around, limit } }) => {
const params = new URLSearchParams()
params.append('limit', String(limit))
if (after != null) {
params.append('after', String(after))
} else if (before != null) {
params.append('before', String(before))
} else if (around != null) {
params.append('around', String(around))
}
return `https://example.com/api/projectsBidirectionalCursor?${params.toString()}`
},
}),
}),
})
```
Limit and offset pagination pattern example
Example of limit and offset pagination:
```ts
export type ProjectsResponse = {
projects: Project[]
numFound: number
serverTime: string
}
type ProjectsInitialPageParam = {
offset: number
limit: number
}
const projectsApi = createApi({
baseQuery,
endpoints: (build) => ({
projectsLimitOffset: build.infiniteQuery<
ProjectsResponse,
void,
ProjectsInitialPageParam
>({
infiniteQueryOptions: {
initialPageParam: {
offset: 0,
limit: 20,
},
getNextPageParam: (
lastPage,
allPages,
lastPageParam,
allPageParams,
) => {
const nextOffset = lastPageParam.offset + lastPageParam.limit
const remainingItems = lastPage?.numFound - nextOffset
if (remainingItems <= 0) {
return undefined
}
return {
...lastPageParam,
offset: nextOffset,
}
},
getPreviousPageParam: (
firstPage,
allPages,
firstPageParam,
allPageParams,
) => {
const prevOffset = firstPageParam.offset - firstPageParam.limit
if (prevOffset < 0) return undefined
return {
...firstPageParam,
offset: firstPageParam.offset - firstPageParam.limit,
}
},
},
query: ({ pageParam: { offset, limit } }) => {
return `https://example.com/api/projectsLimitOffset?offset=${offset}&limit=${limit}`
},
}),
}),
})
```
Schema validation example for infinite queries
Example of using responseSchema and rawResponseSchema with infinite queries:
```ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import * as v from 'valibot'
const pokemonSchema = v.object({
id: v.number(),
name: v.string(),
})
type Pokemon = v.InferOutput<typeof pokemonSchema>
const transformedPokemonSchema = v.object({
...pokemonSchema.entries,
id: v.string(),
})
type TransformedPokemon = v.InferOutput<typeof transformedPokemonSchema>
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com/pokemon' }),
endpoints: (build) => ({
getInfinitePokemon: build.infiniteQuery<Pokemon[], string, number>({
query: ({ queryArg, pageParam }) => `type/${queryArg}?page=${pageParam}`,
argSchema: v.object({
queryArg: v.string(),
pageParam: v.number(),
}),
responseSchema: v.array(pokemonSchema),
}),
getTransformedPokemon: build.infiniteQuery<
TransformedPokemon[],
string,
number
>({
query: ({ queryArg, pageParam }) => `type/${queryArg}?page=${pageParam}`,
argSchema: v.object({
queryArg: v.string(),
pageParam: v.number(),
}),
rawResponseSchema: v.array(pokemonSchema),
transformResponse: (response) =>
response.map((pokemon) => ({
...pokemon,
id: String(pokemon.id),
})),
responseSchema: v.array(transformedPokemonSchema),
}),
}),
})
```
argSchema for infinite queries must include queryArg and pageParam
When using argSchema for infinite query endpoints, the schema must have both queryArg and pageParam fields defined, not just a single arg.