Overview
Infinite queries enable “load more” and infinite scroll patterns by caching multiple “pages” within a single cache entry. Each page is fetched sequentially, and results are accumulated in a pages array.
Infinite queries are ideal for:
Infinite scrolling lists
“Load More” buttons for paginated data
Bi-directional pagination (loading older and newer items)
Social media feeds
Chat message history
Defining an Infinite Query
Use build.infiniteQuery<ResultType, QueryArg, PageParam>() in your API definition:
import { createApi , fetchBaseQuery } from 'ngrx-rtk-query' ;
interface Pokemon {
id : string ;
name : string ;
}
export const pokemonApi = createApi ({
reducerPath: 'pokemonApi' ,
baseQuery: fetchBaseQuery ({ baseUrl: 'https://example.com/pokemon' }),
endpoints : ( build ) => ({
getPokemon: build . infiniteQuery < Pokemon [], string , number >({
infiniteQueryOptions: {
// Required: Initial page parameter
initialPageParam: 1 ,
// Required: Calculate next page parameter
getNextPageParam : ( lastPage , allPages , lastPageParam ) => {
// Return undefined when there are no more pages
return lastPage . length > 0 ? lastPageParam + 1 : undefined ;
},
// Optional: Calculate previous page parameter
getPreviousPageParam : ( firstPage , allPages , firstPageParam ) => {
return firstPageParam > 1 ? firstPageParam - 1 : undefined ;
},
},
query : ({ queryArg , pageParam }) => `/type/ ${ queryArg } ?page= ${ pageParam } ` ,
}),
}),
});
export const { useGetPokemonInfiniteQuery } = pokemonApi ;
Infinite Query Options
Option Type Required Description initialPageParamPageParam✅ Yes The page parameter for the first page getNextPageParam(lastPage, allPages, lastPageParam) => PageParam | undefined✅ Yes Calculates the next page parameter. Return undefined for no more pages getPreviousPageParam(firstPage, allPages, firstPageParam) => PageParam | undefined❌ No Calculates the previous page parameter
If getNextPageParam or getPreviousPageParam returns undefined, it indicates there are no more pages in that direction.
Basic Usage
The infinite query hook returns a signal with pagination state and methods:
import { Component , computed } from '@angular/core' ;
import { useGetPokemonInfiniteQuery } from './api' ;
@ Component ({
selector: 'app-pokemon-list' ,
standalone: true ,
template: `
@if (pokemonQuery.isLoading()) {
<p>Loading...</p>
}
@for (pokemon of allPokemon(); track pokemon.id) {
<div class="pokemon-card">{{ pokemon.name }}</div>
}
<button
[disabled]="!pokemonQuery.hasNextPage() || pokemonQuery.isFetchingNextPage()"
(click)="pokemonQuery.fetchNextPage()">
{{ pokemonQuery.isFetchingNextPage() ? 'Loading...' : 'Load More' }}
</button>
` ,
})
export class PokemonListComponent {
pokemonQuery = useGetPokemonInfiniteQuery ( 'fire' );
// Flatten all pages into a single array
allPokemon = computed (() =>
this . pokemonQuery . data ()?. pages . flat () ?? []
);
}
Infinite Query State
Infinite queries provide all standard query properties plus pagination-specific state:
Standard Query Properties
Property Type Description data(){ pages: T[], pageParams: PageParam[] } | undefinedAll fetched pages and their parameters isLoading()booleantrue during initial fetchisFetching()booleantrue when any request is in flightisSuccess()booleantrue when data is availableisError()booleantrue if the query resulted in an errorerror()SerializedError | FetchBaseQueryErrorError object if query failed
Property Type Description hasNextPage()booleantrue if there are more pages to fetch forwardhasPreviousPage()booleantrue if there are more pages to fetch backwardisFetchingNextPage()booleantrue while fetching the next pageisFetchingPreviousPage()booleantrue while fetching the previous page
Method Type Description fetchNextPage()() => PromiseFetch the next page of results fetchPreviousPage()() => PromiseFetch the previous page of results refetch(options?)(options?) => PromiseRefetch all or specific pages
Data Structure
The data() signal returns an object with two properties:
interface InfiniteData < T , PageParam > {
pages : T []; // Array of page results
pageParams : PageParam []; // Array of page parameters used
}
Example
const data = pokemonQuery . data ();
console . log ( data );
// {
// pages: [
// [{ id: '1', name: 'Charmander' }, { id: '2', name: 'Charmeleon' }], // Page 1
// [{ id: '3', name: 'Charizard' }, { id: '4', name: 'Squirtle' }], // Page 2
// ],
// pageParams: [1, 2] // Page parameters used
// }
Flattening Pages
Use computed() to flatten pages into a single array:
export class PokemonListComponent {
pokemonQuery = useGetPokemonInfiniteQuery ( 'fire' );
// Flatten pages into a single array
allPokemon = computed (() => {
const data = this . pokemonQuery . data ();
return data ?. pages . flat () ?? [];
});
}
Fetch both forward and backward with fetchNextPage() and fetchPreviousPage():
@ Component ({
template: `
<button
[disabled]="!pokemonQuery.hasPreviousPage()"
(click)="pokemonQuery.fetchPreviousPage()">
Load Previous
</button>
@for (pokemon of allPokemon(); track pokemon.id) {
<div>{{ pokemon.name }}</div>
}
<button
[disabled]="!pokemonQuery.hasNextPage()"
(click)="pokemonQuery.fetchNextPage()">
Load More
</button>
`
})
export class PokemonListComponent {
pokemonQuery = useGetPokemonInfiniteQuery ( 'fire' );
allPokemon = computed (() =>
this . pokemonQuery . data ()?. pages . flat () ?? []
);
}
Refetching Pages
The refetch() method supports options to control which pages are refetched:
export class PokemonListComponent {
pokemonQuery = useGetPokemonInfiniteQuery ( 'fire' );
// Refetch all pages
refetchAll () {
this . pokemonQuery . refetch ();
}
// Refetch only the first page (default behavior)
refetchFirst () {
this . pokemonQuery . refetch ({ refetchCachedPages: false });
}
// Refetch all cached pages
refetchAllCached () {
this . pokemonQuery . refetch ({ refetchCachedPages: true });
}
}
Refetch Options
Option Type Default Description refetchCachedPagesbooleanfalseIf true, refetch all cached pages. If false, only refetch the first page
Set refetchCachedPages: true in the hook options to always refetch all pages by default: pokemonQuery = useGetPokemonInfiniteQuery ( 'fire' , {
refetchCachedPages: true ,
});
Query Arguments
Infinite queries support the same argument patterns as regular queries:
Static Arguments
pokemonQuery = useGetPokemonInfiniteQuery ( 'fire' );
Signal Arguments
export class PokemonListComponent {
pokemonType = input . required < string >();
pokemonQuery = useGetPokemonInfiniteQuery ( this . pokemonType );
}
Function Arguments
export class PokemonListComponent {
pokemonType = signal ( 'fire' );
enabledOnly = signal ( true );
pokemonQuery = useGetPokemonInfiniteQuery (() => ({
type: this . pokemonType (),
enabled: this . enabledOnly (),
}));
}
Infinite Query Options
Customize infinite query behavior:
export class PokemonListComponent {
pokemonQuery = useGetPokemonInfiniteQuery ( 'fire' , {
// Initial page parameter for the first fetch
initialPageParam: 0 ,
// Refetch all cached pages on refetch()
refetchCachedPages: true ,
// Standard query options
pollingInterval: 5000 ,
refetchOnFocus: true ,
refetchOnReconnect: true ,
});
}
Implement infinite scrolling with the Intersection Observer API:
import { Component , ElementRef , computed , effect , viewChild } from '@angular/core' ;
import { useGetPokemonInfiniteQuery } from './api' ;
@ Component ({
selector: 'app-pokemon-infinite-scroll' ,
standalone: true ,
template: `
<div class="pokemon-list">
@for (pokemon of allPokemon(); track pokemon.id) {
<div class="pokemon-card">{{ pokemon.name }}</div>
}
@if (pokemonQuery.hasNextPage()) {
<div #sentinel class="sentinel">
@if (pokemonQuery.isFetchingNextPage()) {
<p>Loading more...</p>
}
</div>
}
</div>
` ,
})
export class PokemonInfiniteScrollComponent {
pokemonQuery = useGetPokemonInfiniteQuery ( 'fire' );
sentinel = viewChild < ElementRef < HTMLElement >>( 'sentinel' );
allPokemon = computed (() =>
this . pokemonQuery . data ()?. pages . flat () ?? []
);
constructor () {
effect (( onCleanup ) => {
const sentinelEl = this . sentinel ()?. nativeElement ;
if ( ! sentinelEl ) return ;
const observer = new IntersectionObserver (
( entries ) => {
if ( entries [ 0 ]. isIntersecting && this . pokemonQuery . hasNextPage ()) {
this . pokemonQuery . fetchNextPage ();
}
},
{ threshold: 1.0 }
);
observer . observe ( sentinelEl );
onCleanup (() => observer . disconnect ());
});
}
}
Use cursor-based pagination for more robust infinite scrolling:
interface PokemonPage {
results : Pokemon [];
nextCursor ?: string ;
prevCursor ?: string ;
}
export const pokemonApi = createApi ({
baseQuery: fetchBaseQuery ({ baseUrl: 'https://pokeapi.co/api/v2' }),
endpoints : ( build ) => ({
getPokemon: build . infiniteQuery < PokemonPage , string , string | null >({
infiniteQueryOptions: {
initialPageParam: null ,
getNextPageParam : ( lastPage ) => lastPage . nextCursor ?? undefined ,
getPreviousPageParam : ( firstPage ) => firstPage . prevCursor ?? undefined ,
},
query : ({ queryArg : type , pageParam }) => ({
url: `/type/ ${ type } ` ,
params: { cursor: pageParam ?? undefined },
}),
}),
}),
});
TypeScript Type Signatures
Infinite Query Hook Type
type UseInfiniteQuery < ResultType , QueryArg , PageParam > = (
arg : QueryArg | Signal < QueryArg > | (() => QueryArg ),
options ?: UseInfiniteQueryOptions < ResultType , PageParam >
) => UseInfiniteQueryStateDefaultResult < ResultType , PageParam >;
interface UseInfiniteQueryStateDefaultResult < T , PageParam > {
// Data
data : Signal <{ pages : T [], pageParams : PageParam [] } | undefined >;
currentData : Signal <{ pages : T [], pageParams : PageParam [] } | undefined >;
// Standard query state
isLoading : Signal < boolean >;
isFetching : Signal < boolean >;
isSuccess : Signal < boolean >;
isError : Signal < boolean >;
error : Signal < SerializedError | FetchBaseQueryError | undefined >;
// Pagination state
hasNextPage : Signal < boolean >;
hasPreviousPage : Signal < boolean >;
isFetchingNextPage : Signal < boolean >;
isFetchingPreviousPage : Signal < boolean >;
// Pagination methods
fetchNextPage : () => Promise < InfiniteQueryActionCreatorResult < T >>;
fetchPreviousPage : () => Promise < InfiniteQueryActionCreatorResult < T >>;
refetch : ( options ?: { refetchCachedPages ?: boolean }) => Promise < InfiniteQueryActionCreatorResult < T >>;
}
Best Practices
Flatten Pages with computed
Check hasNextPage Before Fetching
Use Intersection Observer for Infinite Scroll
Return undefined for No More Pages
// ✅ Good - Efficient derived state
export class PokemonListComponent {
pokemonQuery = useGetPokemonInfiniteQuery ( 'fire' );
allPokemon = computed (() =>
this . pokemonQuery . data ()?. pages . flat () ?? []
);
}