Table of contents
Bootstrap the Angular project
Our demo will only render a list of todo items split across pages. We will deliberately skip features like toggling, editing, or adding todos since they fall outside the scope of this guide.
The app in this tutorial is built with Angular v15.2, which was the latest release at the time of writing.
Open a fresh directory and run the following command to generate a new project:
ng new --directory ./ --minimal --inline-template --skip-tests
Accept the default prompts, then open the project in your editor. Clear the boilerplate content from app.component.ts and drop in this minimal version instead:
@Component({
selector: 'app-root',
standalone: true,
template: `
<h1>Simple pagination with NgRx component stores</h1>
`,
styles: []
})
export class AppComponent {
}
Because we’re using standalone components, you can safely delete app.module.ts. Adjust the bootstrap logic in main.ts as shown here:
// main.ts
import { provideHttpClient } from "@angular/common/http";
import { bootstrapApplication } from "@angular/platform-browser";
import { AppComponent } from "./app/app.component";
bootstrapApplication(AppComponent, {
providers: [provideHttpClient()],
}).catch((err) => console.error(err));
At this point, ng serve should work and we’re ready to move forward.
Emulate a backend
To fetch todo items, we’ll rely on the JSON placeholder API as a stand-in for a real server.
First, define the shape of our data in a new file named todo-item.ts:
// todo-item.ts
export interface TodoItem {
userId: number;
id: number;
title: string;
completed: boolean;
}
Next, create a TodoItemService that will be responsible for making the HTTP calls and returning todo items:
// todo-item.service.ts
import { HttpClient, HttpParams } from "@angular/common/http";
import { inject, Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { TodoItem } from "./todo-item";
@Injectable({ providedIn: "root" })
export class TodoItemService {
private readonly _http = inject(HttpClient);
getTodoItems(offset?: number, pageSize?: number): Observable<TodoItem[]> {
const params = new HttpParams({
fromObject: {
_start: offset ?? 0,
_limit: pageSize ?? 10,
},
});
return this._http.get<TodoItem[]>(
"https://jsonplaceholder.typicode.com/todos",
{ params }
);
}
}
Create the todo component
Now that our service is ready, we can build a new TodoItemComponent that consumes it and renders each todo item:
// todo-item.component.ts
import { NgIf } from "@angular/common";
import { Component, Input } from "@angular/core";
import { TodoItem } from "./todo-item";
@Component({
selector: "app-todo-item",
standalone: true,
imports: [NgIf],
template: `
<div *ngIf="todoItem">
<input type="checkbox" [checked]="todoItem.completed" />
<span>#{{ todoItem.id }} - {{ todoItem.title }}</span>
</div>`,
})
export class TodoItemComponent {
@Input() todoItem?: TodoItem;
}
Admittedly, this isn’t the prettiest todo item UI you’ve ever seen.
With all the pieces in place, wire them together to display the first page of results:
// app.component.ts
import { AsyncPipe, NgFor } from "@angular/common";
import { Component, inject } from "@angular/core";
import { TodoItemComponent } from "./todo-item.component";
import { TodoItemService } from "./todo-item.service";
@Component({
selector: "app-root",
standalone: true,
imports: [NgFor, AsyncPipe, TodoItemComponent],
template: `
<h1>Simple pagination with NgRx component stores</h1>
<app-todo-item
*ngFor="let todoItem of todoItems$ | async"
[todoItem]="todoItem"
/>
`,
})
export class AppComponent {
private readonly _todoItemService = inject(TodoItemService);
readonly todoItems$ = this._todoItemService.getTodoItems();
}
After these changes, your homepage should show a handful of todo items:
Add pagination logic
With the app rendering a few items, let’s shift our attention to navigating between pages. We’ll modify AppComponent minimally to expose previous and next buttons:
// app.component.ts
// ...
@Component({
// ...
template: `
<h1>Simple pagination with NgRx component stores</h1>
<app-todo-item
*ngFor="let todoItem of todoItems$ | async"
[todoItem]="todoItem"
/>
<!-- 👇 Pagination buttons -->
<button type="button" aria-label="Previous Page" (click)="onPreviousPage()">
←
</button>
<button type="button" aria-label="Next Page" (click)="onNextPage()">
→
</button>
`,
})
export class AppComponent {
private readonly _todoItemService = inject(TodoItemService);
readonly todoItems$ = this._todoItemService.getTodoItems();
// 👇 Associated handlers
onPreviousPage(): void {}
onNextPage(): void {}
}
The basic solution
A quick and dirty approach is to keep a local variable that tracks pagination state. Every time the user clicks a button, we update that variable and create a new todoItems$ observable:
// app.component.ts
// ...
@Component({
// ...
})
export class AppComponent {
private readonly _todoItemService = inject(TodoItemService);
todoItems$ = this._todoItemService.getTodoItems();
private _pagination = {
offset: 0,
pageSize: 10,
};
onPreviousPage(): void {
this._pagination.offset -= this._pagination.pageSize;
const { offset, pageSize } = this._pagination;
this.todoItems$ = this._todoItemService.getTodoItems(offset, pageSize);
}
onNextPage(): void {
this._pagination.offset += this._pagination.pageSize;
const { offset, pageSize } = this._pagination;
this.todoItems$ = this._todoItemService.getTodoItems(offset, pageSize);
}
}
While this works, it’s far from reactive. The logic ends up scattered across event handlers, and we’re forced to manage state imperatively at each step.
There’s definitely room for improvement — let’s explore a cleaner design.
Tapping into subjects
By leveraging an RxJS BehaviorSubject, we can maintain a single source of truth for our pagination state and respond to any updates immediately, refreshing the todo items accordingly:
// app.component.ts
// ...
@Component({
// ...
})
export class AppComponent {
private readonly _todoItemService = inject(TodoItemService);
// 👇 Introducing a subject holding the pagination details
private readonly _pagination$ = new BehaviorSubject({
offset: 0,
pageSize: 10,
});
// 👇 Reactively update the todo items on pagination change
readonly todoItems$ = this._pagination$.pipe(
switchMap(({ offset, pageSize }) =>
this._todoItemService.getTodoItems(offset, pageSize)
)
);
onPreviousPage(): void {
const { offset, pageSize } = this._pagination$.getValue();
this._pagination$.next({
offset: offset - pageSize,
pageSize,
})
}
onNextPage(): void {
const { offset, pageSize } = this._pagination$.getValue();
this._pagination$.next({
offset: offset + pageSize,
pageSize,
})
}
}
This approach is an improvement, yet the pagination logic and its associated state still reside inside the component itself.
This scenario is a perfect fit for a component store!
Setting Up the Store
Before we can leverage the component store, the corresponding package has to be installed first:
npm install @ngrx/component-store --save
Once that's done, a new file called app.component-store.ts should be created. The state for our feature will be defined right inside this file.
The piece of state we need to hold consists of two parts:
- The list of
TodoItems that should be rendered - The pagination configuration, specifically the current offset and the page size
export interface AppState {
todoItems: TodoItem[];
offset: number;
pageSize: number;
}
To make the store usable, we have to provide an initial state that will serve as the starting point:
const initialState: AppState = {
todoItems: [],
offset: 0,
pageSize: 10,
};
With that initial state defined, we can now instantiate the store and seed it with that data:
@Injectable()
export class AppComponentStore extends ComponentStore<AppState> {
constructor() {
super(initialState);
}
}
In order to actually use the store, it needs to be provided to our AppComponent. This is done through the provideComponentStore function:
// app.component.ts
@Component({
// ...
providers: [
// 👇 Provide our component store to our component
provideComponentStore(AppComponentStore),
]
})
export class AppComponent {
// ...
}
A typical pattern with component stores is to expose a view model, conventionally named vm$, which the component can then subscribe to.
By adopting this approach, we can strip away the old logic that was previously sitting in the template:
// app.component-store.ts
@Injectable()
export class AppComponentStore extends ComponentStore<AppState> {
readonly vm$ = this.select(({ todoItems }) => ({ todoItems }));
constructor() {
super(initialState);
}
}
// app.component.ts
@Component({
selector: "app-root",
standalone: true,
imports: [NgIf, NgFor, AsyncPipe, TodoItemComponent],
template: `
<!-- 👇 Wrap the view model in a container with an async pipe to rerender on new values -->
<ng-container *ngIf="vm$ | async as vm">
<h1>Simple pagination with NgRx component stores</h1>
<!-- 👇 Use the todo items exposed by the view model -->
<app-todo-item
*ngFor="let todoItem of vm.todoItems"
[todoItem]="todoItem"
/>
<button type="button" aria-label="Previous Page" (click)="onPreviousPage()">
←
</button>
<button type="button" aria-label="Next Page" (click)="onNextPage()">
→
</button>
</ng-container>
`,
providers: [provideComponentStore(AppComponentStore)],
})
export class AppComponent {
// 👇 Consume the component store and its API instead of
// handling the logic here
private readonly _componentStore = inject(AppComponentStore);
readonly vm$ = this._componentStore.vm$;
onPreviousPage(): void {}
onNextPage(): void {}
}
That's all we need to get started.
Now that the store is in place, we can migrate our pagination logic into it.
This type of store can be organized much like traditional ones, splitting the code into three distinct areas:
- Selectors
- Effects
- Reducer
In the context of a component store, selectors are often not explicitly defined since the view model takes over that responsibility.
Still, we can trigger effects that will ultimately modify the state of the store.
Following this pattern, let's emit events whenever the offset changes, and adjust the state accordingly:
@Injectable()
export class AppComponentStore extends ComponentStore<AppState> {
private readonly _todoItemService = inject(TodoItemService);
readonly vm$ = this.select(({ todoItems }) => ({ todoItems }));
constructor() {
super(initialState);
}
// 👇 Effect loading the todo items
readonly loadNextPage = this.effect((trigger$: Observable<void>) => {
return trigger$.pipe(
withLatestFrom(this.select((state) => state)),
map(([, state]) => state),
tap(({ offset }) => this.updateOffset(offset + 1)),
switchMap(({ offset, pageSize }) =>
this._todoItemService.getTodoItems(offset * pageSize, pageSize).pipe(
tapResponse(
(todoItems: TodoItem[]) => this.updateTodoItems(todoItems),
() => console.error("Something went wrong")
)
)
)
);
});
// 👇 Updaters for our state
private readonly updateOffset = this.updater(
(state: AppState, offset: number) => ({
...state,
offset,
})
);
private readonly updateTodoItems = this.updater(
(state: AppState, todoItems: TodoItem[]) => ({
...state,
todoItems,
})
);
}
The effect for the previous page follows a very similar structure, go ahead and write it on your own!
With the effects defined, they can be invoked from the component:
// app.component.ts
@Component({
// ...
})
export class AppComponent {
private readonly _componentStore = inject(AppComponentStore);
readonly vm$ = this._componentStore.vm$;
onPreviousPage(): void {}
onNextPage(): void {
this._componentStore.loadNextPage();
}
}
There's a catch though. When the page is reloaded, nothing shows up until onNextPage is triggered.
If we trace the component store's lifecycle, it becomes clear that no data is being fetched before the first change is requested.
To fix this, we can split the solution into two steps:
The first step involves separating the initial page load from the subsequent offset changes, creating two dedicated effects:
// app.component-store.ts
readonly loadPage = this.effect((trigger$: Observable<void>) => {
return trigger$.pipe(
withLatestFrom(this.select((state) => state)),
map(([, state]) => state),
switchMap(({ offset, pageSize }) =>
this._todoItemService.getTodoItems(offset * pageSize, pageSize).pipe(
tapResponse(
(todoItems: TodoItem[]) => this.updateTodoItems(todoItems),
() => console.error("Something went wrong")
)
)
)
);
});
readonly loadNextPage = this.effect((trigger$: Observable<void>) => {
return trigger$.pipe(
withLatestFrom(this.select((state) => state.offset)),
map(([, state]) => state),
tap((offset) => this.updateOffset(offset + 1)),
tap(() => this.loadPage())
);
});
The second step is to leverage the NgRx component store lifecycle hooks, which are covered in detail in this article by @brandontroberts
According to that resource, it's possible to run an action right after the state has been initialized, or after the store itself has been set up.
Loading the initial page right after the store is initialized seems like the perfect use case — let's implement that:
@Injectable()
export class AppComponentStore
extends ComponentStore<AppState>
// 👇 Implement the hook
implements OnStoreInit
{
private readonly _todoItemService = inject(TodoItemService);
readonly vm$ = this.select(({ todoItems }) => ({ todoItems }));
constructor() {
super(initialState);
}
// 👇 Load the page once the store is initialized
ngrxOnStoreInit() {
this.loadPage();
}
// ...
}
After refreshing the browser, the fix becomes visible: the pagination setup, powered by the component store, is now working as expected.
If you want to push this further, here are some ideas to explore:
- Write the effect that handles the previous page
- Guard against offset boundaries, such as preventing it from dropping below zero
- Introduce loading and error states to the UI
- Allow the page size to be modified
The full source code is available in the associated GitHub repository
Stay tuned for the next article, where we'll explore other applications, including how to use generics to build a reusable component store across the entire application.
Thanks for reading, and as always, happy coding!
Photo by Roman Trifonov on Unsplash


