Angular 9
Pagination
Observables
AsyncPipe
Web Development

How to combine a pagination with Observables and the AsyncPipe in Angular 9?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Pagination fits naturally with Angular observables when the current page is treated as a stream of state instead of as a mutable field with manual subscriptions. The AsyncPipe then renders the latest page result and handles subscription cleanup automatically.

The practical pattern is to represent the page number with an observable, turn page changes into HTTP requests with switchMap, and expose a single observable view model to the template.

Model Page Changes Reactively

Start with a service that fetches one page from the backend:

typescript
1import { HttpClient, HttpParams } from '@angular/common/http';
2import { Injectable } from '@angular/core';
3import { Observable } from 'rxjs';
4
5export interface PagedResponse<T> {
6  items: T[];
7  page: number;
8  pageSize: number;
9  total: number;
10}
11
12@Injectable({ providedIn: 'root' })
13export class ProductsService {
14  constructor(private http: HttpClient) {}
15
16  getPage(page: number, pageSize: number): Observable<PagedResponse<string>> {
17    const params = new HttpParams()
18      .set('page', String(page))
19      .set('pageSize', String(pageSize));
20
21    return this.http.get<PagedResponse<string>>('/api/products', { params });
22  }
23}

In the component, model the current page as a BehaviorSubject:

typescript
1import { ChangeDetectionStrategy, Component } from '@angular/core';
2import { BehaviorSubject } from 'rxjs';
3import { shareReplay, switchMap } from 'rxjs/operators';
4import { ProductsService } from './products.service';
5
6@Component({
7  selector: 'app-products',
8  templateUrl: './products.component.html',
9  changeDetection: ChangeDetectionStrategy.OnPush
10})
11export class ProductsComponent {
12  private readonly page$ = new BehaviorSubject<number>(1);
13  readonly pageSize = 10;
14
15  readonly vm$ = this.page$.pipe(
16    switchMap(page => this.productsService.getPage(page, this.pageSize)),
17    shareReplay(1)
18  );
19
20  constructor(private productsService: ProductsService) {}
21
22  goToPage(page: number): void {
23    this.page$.next(page);
24  }
25
26  next(page: number): void {
27    this.page$.next(page + 1);
28  }
29
30  previous(page: number): void {
31    if (page > 1) {
32      this.page$.next(page - 1);
33    }
34  }
35}

switchMap is the important operator here. If the user clicks pages quickly, older requests are abandoned and only the latest page result is allowed to update the UI.

Bind the Result with AsyncPipe

The template can stay declarative:

html
1<ng-container *ngIf="vm$ | async as vm">
2  <ul>
3    <li *ngFor="let item of vm.items">{{ item }}</li>
4  </ul>
5
6  <button (click)="previous(vm.page)" [disabled]="vm.page === 1">Previous</button>
7  <span>Page {{ vm.page }}</span>
8  <button
9    (click)="next(vm.page)"
10    [disabled]="vm.page * vm.pageSize >= vm.total">
11    Next
12  </button>
13</ng-container>

The AsyncPipe subscribes to vm$, pushes the latest response into the template, and unsubscribes when the component is destroyed. That removes most of the manual cleanup code that older Angular examples often rely on.

Combine Pagination with Other State

Real screens usually have more than page number. Search terms, sort order, filters, and refresh triggers can all be modeled as streams and combined with pagination:

typescript
1combineLatest([this.page$, this.searchTerm$]).pipe(
2  switchMap(([page, term]) =>
3    this.productsService.search(term, page, this.pageSize)
4  )
5);

This keeps the component honest about what drives the request. Instead of manually calling loadPage() from several places, the request becomes a function of reactive state.

Why This Pattern Is Preferable

The imperative version usually means a mutable page field, a loadPage() method, explicit subscriptions, and side effects that assign values into component fields. That works for a toy example, but it becomes hard to maintain once quick navigation, filtering, and cancellation matter.

Using observables plus AsyncPipe keeps the flow simple: state streams in, HTTP streams out, and the template renders the latest view model. That is easier to test and easier to extend.

Common Pitfalls

  • Using mergeMap instead of switchMap, which can let stale responses overwrite newer page results.
  • Subscribing manually in the component and also using AsyncPipe, creating duplicated data flow.
  • Omitting metadata such as total and pageSize, which makes disabling navigation controls awkward.
  • Treating pagination as imperative mutable state instead of as part of the observable pipeline.

Summary

  • Represent the current page as an observable, commonly with BehaviorSubject.
  • Use switchMap so only the latest page request updates the UI.
  • Expose one observable view model and bind it with AsyncPipe.
  • Compose pagination with search or sorting by combining streams rather than calling load methods manually.
  • Prefer the observable plus AsyncPipe pattern over subscription-heavy imperative pagination code.

Course illustration
Course illustration

All Rights Reserved.