Documentation Index
Fetch the complete documentation index at: https://mintlify.com/SaulMoro/ngrx-rtk-query/llms.txt
Use this file to discover all available pages before exploring further.
This guide demonstrates how to implement pagination using lazy queries with preferCacheValue for optimal performance.
Lazy Query Pattern
Use useLazyGetPostsQuery to manually trigger queries for specific pages:
app/posts/paginated-posts.component.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { useLazyGetPostsQuery } from './api';
@Component({
selector: 'app-paginated-posts',
standalone: true,
imports: [FormsModule, RouterLink],
template: `
<section>
<h1>Posts - Page {{ currentPage() }}</h1>
@if (postsQuery.isLoading()) {
<p>Loading...</p>
}
@if (postsQuery.isError()) {
<p>Error loading posts</p>
}
@if (postsQuery.data(); as posts) {
<ul>
@for (post of posts; track post.id) {
<li>
<a [routerLink]="['/posts', post.id]">{{ post.name }}</a>
</li>
}
</ul>
}
<div class="pagination-controls">
<button
[disabled]="currentPage() === 1 || postsQuery.isFetching()"
(click)="goToPage(currentPage() - 1)"
>
Previous
</button>
<span>Page {{ currentPage() }}</span>
<button
[disabled]="postsQuery.isFetching()"
(click)="goToPage(currentPage() + 1)"
>
Next
</button>
</div>
</section>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PaginatedPostsComponent {
readonly postsQuery = useLazyGetPostsQuery();
readonly currentPage = signal(1);
constructor() {
// Load first page on init
this.goToPage(1);
}
goToPage(page: number): void {
this.currentPage.set(page);
this.postsQuery(
{ page, limit: 10 },
{ preferCacheValue: true }
);
}
}
preferCacheValue: true prevents unnecessary refetches when navigating to previously visited pages. The query will only fetch if the page is not in cache.
Define an endpoint that accepts pagination parameters:
import { createApi, fetchBaseQuery } from 'ngrx-rtk-query';
import { type Post } from './post.model';
interface PaginationParams {
page: number;
limit: number;
}
interface PaginatedResponse {
posts: Post[];
total: number;
page: number;
totalPages: number;
}
export const postsApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'http://api.localhost.com' }),
tagTypes: ['Posts'],
endpoints: (build) => ({
getPosts: build.query<PaginatedResponse, PaginationParams>({
query: ({ page, limit }) => ({
url: '/posts',
params: { page, limit },
}),
providesTags: (result) =>
result
? [
...result.posts.map(({ id }) => ({ type: 'Posts', id }) as const),
{ type: 'Posts', id: 'PARTIAL-LIST' },
]
: [{ type: 'Posts', id: 'PARTIAL-LIST' }],
}),
addPost: build.mutation<Post, Partial<Post>>({
query: (body) => ({
url: '/posts',
method: 'POST',
body,
}),
// Invalidate all paginated queries
invalidatesTags: [{ type: 'Posts', id: 'PARTIAL-LIST' }],
}),
}),
});
export const { useLazyGetPostsQuery, useAddPostMutation } = postsApi;
Show page numbers with active state:
app/posts/advanced-pagination.component.ts
import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';
import { RouterLink } from '@angular/router';
import { useLazyGetPostsQuery } from './api';
@Component({
selector: 'app-advanced-pagination',
standalone: true,
imports: [RouterLink],
template: `
<section>
<h1>Posts</h1>
@if (postsQuery.data(); as response) {
<ul>
@for (post of response.posts; track post.id) {
<li>
<a [routerLink]="['/posts', post.id]">{{ post.name }}</a>
</li>
}
</ul>
<div class="pagination">
<button
[disabled]="currentPage() === 1 || postsQuery.isFetching()"
(click)="goToPage(currentPage() - 1)"
>
Previous
</button>
@for (page of pageNumbers(); track page) {
<button
class="page-number"
[class.active]="page === currentPage()"
[disabled]="postsQuery.isFetching()"
(click)="goToPage(page)"
>
{{ page }}
</button>
}
<button
[disabled]="currentPage() === totalPages() || postsQuery.isFetching()"
(click)="goToPage(currentPage() + 1)"
>
Next
</button>
</div>
<p class="meta">
Showing {{ response.posts.length }} of {{ response.total }} posts
</p>
}
</section>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AdvancedPaginationComponent {
readonly postsQuery = useLazyGetPostsQuery();
readonly currentPage = signal(1);
readonly pageSize = signal(10);
readonly totalPages = computed(() => {
const data = this.postsQuery.data();
return data?.totalPages ?? 1;
});
readonly pageNumbers = computed(() => {
const total = this.totalPages();
const current = this.currentPage();
const delta = 2; // Pages to show on each side of current
const range: number[] = [];
const left = Math.max(1, current - delta);
const right = Math.min(total, current + delta);
for (let i = left; i <= right; i++) {
range.push(i);
}
return range;
});
constructor() {
this.goToPage(1);
}
goToPage(page: number): void {
this.currentPage.set(page);
this.postsQuery(
{ page, limit: this.pageSize() },
{ preferCacheValue: true }
);
}
}
Clear cached pages when needed:
export class PaginatedPostsComponent {
readonly postsQuery = useLazyGetPostsQuery();
resetCache(): void {
// Reset the query state
this.postsQuery.reset();
// Reload first page
this.goToPage(1);
}
}
Use reset() when you need to clear all cached pages, such as after a filter change or when implementing a “Refresh” button.
For cursor-based APIs, track the cursor instead of page numbers:
app/posts/cursor-pagination.component.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { useLazyGetPostsQuery } from './api';
interface CursorParams {
cursor?: string;
limit: number;
}
interface CursorResponse {
posts: Post[];
nextCursor?: string;
prevCursor?: string;
}
@Component({
selector: 'app-cursor-pagination',
template: `
<section>
@if (postsQuery.data(); as response) {
<ul>
@for (post of response.posts; track post.id) {
<li>{{ post.name }}</li>
}
</ul>
<div class="pagination">
<button
[disabled]="!response.prevCursor || postsQuery.isFetching()"
(click)="loadPage(response.prevCursor!)"
>
Previous
</button>
<button
[disabled]="!response.nextCursor || postsQuery.isFetching()"
(click)="loadPage(response.nextCursor!)"
>
Next
</button>
</div>
}
</section>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CursorPaginationComponent {
readonly postsQuery = useLazyGetPostsQuery();
readonly cursors = signal<string[]>([]);
constructor() {
this.loadPage();
}
loadPage(cursor?: string): void {
this.postsQuery(
{ cursor, limit: 10 },
{ preferCacheValue: true }
);
}
}
Best Practices
Cache Strategy
- Use
preferCacheValue: true for pagination to avoid refetching visited pages
- Invalidate paginated caches with a common tag like
PARTIAL-LIST
- Consider cache TTL for data that changes frequently
User Experience
- Disable navigation buttons during loading
- Show loading states for better feedback
- Preserve scroll position when navigating (Angular router handles this)
- Consider skeleton loaders for initial page load
// ✅ Good: Fine-grained reactivity
postsQuery.isFetching()
postsQuery.data()
// ❌ Avoid: Coarse-grained (re-renders on any change)
const query = postsQuery();
query.isFetching
query.data
For infinite scroll patterns where all pages are displayed together, see the Infinite Scroll guide.