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!