How to add infinite scroll to Angular material list


If you've ever had to deal with large lists in your web app, you're probably familiar with infinite scroll. Infinite scroll is a common UI/UX solution to the problem of presenting a large list to your users as when they need it. In this article, we're going to add an infinite scroll in Angular using the Angular CDK library.

Video tutorial

If you're more of a video person, check out the video version above. Otherwise, just continue below :)

The end result will be the following material list, which loads up more items when you scroll down to the end. And most importantly, is also performant for the users using virtual scrolling.

Infinite scrolling list with loader

Our final result

Let's get started then!

Setting up our project

To setup our project, we first create a new Angular app and add Angular Material components to the same. This can be done by using the following commands.

ng new angular-infinite scroll
ng add @angular/material

Then, let's add our required modules to the app.module.ts file.

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    ScrollingModule,
    MatToolbarModule,
    MatListModule,
    MatDividerModule,
    MatButtonModule,
    MatIconModule,
    MatMenuModule,
    MatProgressSpinnerModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Generating the list items

We're going to generate a random array to render our list using a simple loop. Our list will contain a title, some content and an image avatar. We declare a new listItems array in our app.component.ts file. Then, we create a function to add more items to our list.

listItems = [];

fetchMore(): void {

    const images = ['IuLgi9PWETU', 'fIq0tET6llw', 'xcBWeU4ybqs', 'YW3F-C5e8SE', 'H90Af2TFqng'];

    const newItems = [];
    for (let i = 0; i < 20; i++) {
      const randomListNumber = Math.round(Math.random() * 100);
      const randomPhotoId = Math.round(Math.random() * 4);
      newItems.push({
        title: 'List Item ' + randomListNumber,
        content: 'This is some description of the list - item # ' + randomListNumber,
        image: `https://source.unsplash.com/${images[randomPhotoId]}/50x50`
      });
    }

    this.listItems = [...this.listItems, ...newItems];

  }

The title, content and the image for each item are generated randomly. Images are selected from a list of Unsplash images. We use the spread operator syntax to append the new items to our existing list array.

We call this function in our ngOnInit method to populate the list initially as well.

The Angular Material List

For the template we use the Angular Material List component as follows.

<mat-list class="content">
  <mat-list-item *ngFor="let item of listItems">
    <img matListAvatar [src]="item.image" />
    <h3 matLine>{{item.title}}</h3>
    <p matLine>
      <span> {{item.content}} </span>
    </p>
    <button mat-icon-button [matMenuTriggerFor]="itemMenu">
      <mat-icon> more_vert </mat-icon>
    </button>
    <mat-divider></mat-divider>
  </mat-list-item>
</mat-list>

<mat-menu #itemMenu="matMenu">
  <button mat-menu-item>Option 1</button>
  <button mat-menu-item>Option 2</button>
  <button mat-menu-item>Option 3</button>
</mat-menu>

I've also added an Angular Material Menu here on each item, so that we can see the performance degradation when we render a large list (more on that below!). For more about how to set up an Angular Material list and the menu, you can visit the Angular Material components official documentation.

We also add a bit of styling to fix the list height. This way we always get a fixed number of items visible at one time.

.content {
  height: calc(100vh - 64px);
  overflow: auto;
}

As a result, now we have a small 20-item list which scrolls nicely and has a menu icon with each item, which opens up an Angular Material menu.

Performance issues with infinite scroll

Before moving on to adding more functionality, we need to re assess the implications of having an infinite scroll in your Angular app.

Infinite scroll is a nice feature but it doesn't come without its problems. One of the main problems is that as new content loads in the list, it will keep adding to your DOM and increasing the memory requirement.

To test this, I used the same list template above and simply initialized the code with a list with thousands of items to see how it affects performance. I kept an eye on the memory tab in Chrome Developer Tools and also observed the UI for any lags.

Both of these indicated worsening performance as I increased the initial list size. Here's a snapshot of the heap memory when the list size was 5000 and a GIF showing the UI degradation for the menu opening.

I know what you're thinking! Who would load 5000 items?

Well, granted it is an extreme case. But it is just an indication of the performance problems that can crop up with infinite scroll in Angular (or any web app). As the list items become more complex to render or the app has more UI elements to render, the performance degradation will start sooner and will not be pretty.

Therefore, we need a strategy or technique to deal with this problem. And virtual scrolling seems to be the answer to our prayers!

Enter virtual scrolling

Virtual scrolling is a useful strategy to use when rendering large lists in web apps. In simple words it means only those items which are visible to the user at any time are rendered. Other items which are not visible are removed from the DOM. This is also how mobile phones render large lists because performance problems are especially visible on mobile devices with limited memory.

Fortunately for us, Angular CDK provides a virtual scroller component out of the box. Let's use the same for our sample app!

Adding the CDK virtual scroller

To add the Angular CDK virtual scroller, we first need to include the ScrollingModule in our app.module.ts file.

Then we just modify our app.component.html file like this.

<cdk-virtual-scroll-viewport #scroller itemSize="72" class="content">
  <mat-list>
    <ng-container *cdkVirtualFor="let item of listItems">
      <mat-list-item> ... </mat-list-item>
    </ng-container>
  </mat-list>
</cdk-virtual-scroll-viewport>

The Angular Material List code remains completely the same. All we did was to replace the parent container with cdk-virtual-scroll-viewport. And instead of *ngFor we're using *cdkVirtualFor to loop through our list items.

The itemSize input is important here. Currently, the Angular CDK virtual scroller only supports fixed size items in its list. So here we're just passing in the height of a single item in our list in pixels.

Testing the virtual scrolling

If you test now, you won't feel any difference at all in the list. But behind the scenes, the virtual scroller is adding and removing items as you scroll up and down. To see this in action, you can use the Chrome Developer tools or similar tooling to inspect the list items in the HTML. The number of list items in the DOM at any time will always remain the same now, whether you have a list of 50 elements or 5000 elements!

Detecting the end of the scroll using RxJS

To append new items to the list as the user scrolls to the bottom, we need to add some RxJS magic to our app. RxJS is a library which helps us work with asynchronous data streams. Since scrolling events are one example of a stream of data, we can leverage the power of RxJS to detect whether the user has reached the end of our list.

The Angular CDK scroll viewport provides us with an elementScrolled event which we can conveniently use to build an RxJS stream. We can get the virtual scroller instance from the template by adding a ViewChild decorator in our app.component.ts file.

  @ViewChild('scroller') scroller: CdkVirtualScrollViewport;

Then, we build up a stream like the following.

this.scroller.elementScrolled().pipe(
      map(() => this.scroller.measureScrollOffset('bottom')),
      pairwise(),
      filter(([y1, y2]) => (y2 < y1 && y2 < 140)),
      throttleTime(200)
    ).subscribe(() => {
      this.ngZone.run(() => {
        this.fetchMore();
      });
    }

Breaking down the RxJS stream

Let's go through the steps in this stream briefly. For each time the user scrolls the list

  1. We get the scroll offset in pixels from the bottom of the list. For this we use a method provided by the virtual scroller called measureScrollOffset
  2. We then use the pairwise operator to get this offset in pairs, so that we can see whether it is increasing or decreasing
  3. Then we add a filter to the stream and only allow it to continue when y2 < y1, i.e. the user is scrolling down and also when the offset is near to the bottom (less than 140 pixels i.e. two items in our list)
  4. Lastly, we add a throttleTime operator, so that we don't get repeated scroll events and just one in 200 ms

Pretty cool, huh? It's amazing how convenient RxJS is in converting asynchronous events into more meaningful events.

For a more detailed introduction to RxJS and its basic operators, you might find my article on the same useful!

https://zoaibkhan.com/blog/rxjs-in-angular-creating-a-weather-app/

Last, but not the least, we subscribe to this stream and call our fetchMore()function there. We do this inside an ngZone run function because the CDK virtual scroller runs outside the ngZone for performance reasons.

Great! If you test out now, you should see new items being added as soon as you reach the end of the scroll. Mission accomplished!

Infinite scrolling list without loader

Finishing touch: Add the loading indicator

In a more typical use case, you'll obviously be getting more list items from an API call which can bring in a delay in the process. So from a UX point of view, it is good to have a small progress spinner as soon you reach the end of the scroll and your app is fetching new items.

Let's add that to our app as a finishing touch!

First, we'll add a loading boolean variable to represent whether data is being fetched. Then, well modify our fetching function to add a little delay using the timer function in RxJS (just to simulate network delay).

this.loading = true;
timer(1000).subscribe(() => {
  this.loading = false;
  this.listItems = [...this.listItems, ...newItems];
});

Next, we simply add the Angular Material Progress Spinner and some styling to make it appear at the bottom of the list.

<mat-list>
  .......
  <div class="spinner-item">
    <mat-progress-spinner [mode]="'indeterminate'" [diameter]="50">
    </mat-progress-spinner>
  </div>
</mat-list>
.spinner-item {
  display: grid;
  place-items: center;
  margin-top: 10px;
}

And we're done! If you run ng serve now, you'll see a nice infinite scroll list, which keeps loading more items as you scroll and shows a loading icon as well!

Infinite scrolling list with loader

Infinite scrolling list with loading icon

Conclusion

As you can see now, adding an infinite scroll to your Angular app can be a bit complex. But in cases where you have to show large lists to the user and also keep your app fast and give good performance, it is almost necessary to use it. Angular CDK Virtual Scroller helps us out greatly in such cases.

I recently shifted one of my client's complex infinite scroll list in Angular to use virtual scrolling and it has improved performance 'tremendously' in his own words. I hope it helps you in your own projects as well!

The full code for this tutorial can be found on this link.

Thanks for reading!

Bye :)

Check out my Angular and Firebase Authentication crash course

thumbnail
Angular Firebase Authentication: Create Full Sign Up App

Use Angular 16, Angular Material and Firebase Authentication, Firestore and Storage to create a complete Sign Up App!

You may also like...