ANGULAR START v19 has launched! ... Get 25% off LEARN MORE

👋 Hey there!

I'm Josh Morony, and this website is a collection of my free content centered around creating modern Angular applications using the latest Angular features, and concepts like reactive and declarative code.

Tutorial hero
Lesson icon

Reactive Infinite Scrolling with Angular and Ionic

Originally published March 02, 2022 Time 6 mins

In a previous video, we discussed how to create reactive pagination in an Angular application without manually subscribing to observable streams. In this blog post, we’ll take it a step further by implementing reactive infinite scrolling in an Angular and Ionic application.

This article is adapted from the following video:

Infinite Scroll

Infinite scroll is a common scenario in mobile apps. Instead of specifying a specific page number and displaying the data for just that page, we want the list to keep growing with new page data that gets loaded in when we get to the bottom of the list.

Our solution will be mostly similar to the original solution for reactive pagination. In theory, we just need to have our PassengerService return the data for everything up to and including a specific page when a page change occurs, rather than just the data for that specific page.

Handling the Infinite Scroll Component

We’re using the infinite scroller from Ionic, which emits an event when the infinite scroll is triggered, so we know when to go and load our data. We can also use that event to notify the infinite scroll component when we have finished loading so that it knows to remove the loading indicator at the bottom of the list.

<ion-header>
  <ion-toolbar>
    <ion-title *ngIf="currentPage$ | async as currentPage"> Current page: {{currentPage}} </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ng-container *ngIf="currentPageData$ | async as passengers; else loading">
    <ion-list>
      <ion-item *ngFor="let passenger of passengers; trackBy: trackByFn">
        <ion-label>{{passenger.data.author}}</ion-label>
      </ion-item>
    </ion-list>
    <ion-infinite-scroll threshold="100px" (ionInfinite)="loadData($event)">
      <ion-infinite-scroll-content loadingSpinner="bubbles" loadingText="Loading more data..."> </ion-infinite-scroll-content>
    </ion-infinite-scroll>
  </ng-container>

  <ng-template #loading>
    <ion-list>
      <ion-item *ngFor="let arr of [1, 2, 3, 4]">
        <ion-label><ion-skeleton-text animated></ion-skeleton-text></ion-label>
      </ion-item>
    </ion-list>
  </ng-template>
</ion-content>

First, I’ve modified the passenger service to use the Reddit API instead of the example API we were using before:

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class PassengerService {
  #pageSize = 15;

  constructor(private http: HttpClient) {}

  getPassengerData(page: number) {
    return this.http.get(`https://www.reddit.com/r/gifs/hot/.json?limit=${page * this.#pageSize}`).pipe(map((res: any) => res.data.children));
  }
}

The reason is that the other test API didn’t keep the data returned consistent for different page sizes, which made our pagination a bit wonky. So instead, to grab our passengers names, I am just hitting a random subreddit using the Reddit API to return some real data - we will just use the names of the authors of posts. You can see that I am supplying a page limit here, which is just whatever the current page is multiplied by whatever we want the page size to be. This will return us one big chunk of data for everything up to and including the page we have specified.

I’ve also added the infinite scroll component, and that triggers this loadData method when the infinite scroll event is triggered:

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  currentPage$ = new BehaviorSubject<number>(1);
  #currentInfiniteEvent: InfiniteScrollCustomEvent;

  currentPageData$ = this.currentPage$.pipe(
    switchMap((currentPage) =>
      this.passengerService.getPassengerData(currentPage)
    ),
    tap(() => {
      if (this.#currentInfiniteEvent) {
        this.#currentInfiniteEvent.target.complete();
        this.#currentInfiniteEvent = null;
      }
    })
  );

  constructor(private passengerService: PassengerService) {}

  loadData(event: Event) {
    this.#currentInfiniteEvent = event as InfiniteScrollCustomEvent;
    this.currentPage$.next(this.currentPage$.value + 1);
  }

  trackByFn(index: number, element: any) {
    return element.data.id;
  }

This loadData method is basically the same idea as the previous video - we just emit a new value on our behavior subject incrementing the current page by 1. However, we are also keeping a reference to the event that the infinite scroll emits that we need to use to indicate when the load is finished.

Now, if you’re used to imperative programming, the first solution you might think of to call this complete method on the event once the data has finished loading is just to subscribe to currentPageData$ inside of loadData. This would emit a value when the new data loads, and we could trigger the complete method on the event from within that subscribe.

But again, if we are coding reactively, we want to avoid manually subscribing. We can deal with this quite nicely by just piping on the tap operator to our existing stream:

  currentPageData$ = this.currentPage$.pipe(
    switchMap((currentPage) =>
      this.passengerService.getPassengerData(currentPage)
    ),
    tap(() => {
      if (this.#currentInfiniteEvent) {
        this.#currentInfiniteEvent.target.complete();
        this.#currentInfiniteEvent = null;
      }
    })
  );

The tap operator is a good way to create side effects on streams - it doesn’t actually modify the stream in any way, it’s basically just like a way to spy on what is going on in the stream at a certain point. So what we do is after we run our switchMap, which will be when the data has been loaded in, we use tap to call the complete method on the infinite scroll event.

Also, if you’re wondering about the trackBy function, this is just a way for us to explicitly tell Angular how to keep track of specific elements we are displaying in our ngFor list - we tie this to the unique id of each post being returned.

By default, Angular will keep track of elements in an ngFor by reference, and it uses this to help optimize rendering the list when it changes, but with changing data sets (like reloading data in from an API) those references can be lost and so the entire list will need to be re-rendered. This just gives Angular a helping hand, and says:

“hey, this property right here on the element is unique and won’t change, so you can just use that to keep track of things”

And that’s it, now we have reactive infinite scrolling in our app!

Learn to build modern Angular apps with my course