Search Lessons, Code Snippets, and Videos
search by algolia
X
#native_cta# #native_desc# Sponsored by #native_company#

Infinite Scroll in Angular With Firebase Data

Episode 37 written by Jeff Delaney
full courses and content on fireship.io

When you have a large collection of information in your database, it’s often not practical to query it in a single go. We could implement pagination, but that usually doesn’t deliver a great user experience for modern progressive web apps. A better approach is to use the scroll position of the user to query the next batch of data, aka infinite scroll.

In this lesson, I am going to show you how to asynchronously combine multiple array Observables from AngularFire2, then display them in a component based on the user’s scroll position.

Infinite scroll with Angular4 and Firebase

Adding the ngx-infinite-scroll Package

Keeping track of the scroll position of a user is no joke - that’s why we’re going to use the excellent ngx-infinite-scroll package. This gives us a reliable way to listen for scroll events, while also throttling the frequency of events to prevent making millions of unnecessary queries to Firebase.

npm install ngx-infinite-scroll --save

Then add it your AppModule (or any module that is using it).

/// ...omitted

import { InfiniteScrollModule } from 'ngx-infinite-scroll';


@NgModule({
imports: [
InfiniteScrollModule
],
/// ...omitted
})

Retrieving Data from Firebase

Our service will be responsible for querying data with AngularFire2. For this demo, I have a collection of eight movies, which will be loaded in batches of two.

Database Structure

Our database structure is about as simple as it gets.

movies
$movieKey
title: string
year: number
image: string

movie.service.ts

ng g service movie

When we query a FirebaseListObservable we get an array of objects. In order to know where to start this query, we need the key from the previous batch of movies. Therefore, our service function will accept an argument for the previous key, allowing us to offset the query using the startAt property.

import { Injectable } from '@angular/core';
import { AngularFireDatabase, FirebaseListObservable } from 'angularfire2/database';

@Injectable()
export class MovieService {

constructor(private db: AngularFireDatabase) { }

getMovies(batch, lastKey?) {
let query = {
orderByKey: true,
limitToFirst: batch,
}

if (lastKey) query['startAt'] = lastKey

return this.db.list('/movies', {
query
})
}
}

Implementing Infinite Scroll Component

ng g component movies-list

movies-list.component.html

Let’s start in the HTML of the component. Directly after our collection of movies, we add the infiniteScroll directive provided by ngx-infinite-scroll. We set the throttle to 1000, which means the event will only fire ever 1000ms (1 second). This is important because we would could hypothetically send thousands of queries to Firebase in just a few seconds of scrolling. We also add a conditional loading spinner that will appear as long as there are still records left to retrieve in the database.

<h1>Movies</h1>

<div *ngFor="let movie of movies | async">


<h2>{{ movie?.title }}</h2>
<p>Released in {{ movie?.year }}</p>
<img [src]="movie?.image" width="120px">

</div>

<div
infiniteScroll
[infiniteScrollDistance]="2"
[infiniteScrollThrottle]="1000"
(scrolled)="onScroll()">
</div>

<div *ngIf="!finished">
Loading more movies...
</div>


<div *ngIf="finished">
End of database... That's all folks!
</div>

movies-list.component.ts

This implementation pulls new data from firebase one query at a time and saves the returned values into a BehaviorSubject. It works in the following logical steps

  1. Retrieve initial movies from the database during ngOnInit()
  2. Grab 1 extra record and save its key
  3. When scroll event fires, grab another batch of movies offset by the lastKey.
  4. When lastKey equals last movie in a new batch, we have reached the end of the database.
import { Component, OnInit} from '@angular/core';
import { MovieService } from '../movie.service'

import { BehaviorSubject } from 'rxjs/BehaviorSubject'
import * as _ from 'lodash'

@Component({
selector: 'movies-list',
templateUrl: './movies-list.component.html',
styleUrls: ['./movies-list.component.scss']
})
export class MoviesListComponent implements OnInit {

movies = new BehaviorSubject([]);

batch = 2 // size of each query
lastKey = '' // key to offset next query from
finished = false // boolean when end of database is reached

constructor(private movieService: MovieService) { }

ngOnInit() {
this.getMovies()
}

onScroll () {
console.log('scrolled!!')
this.getMovies()
}

private getMovies(key?) {
if (this.finished) return

this.movieService
.getMovies(this.batch+1, this.lastKey)
.do(movies => {

/// set the lastKey in preparation for next query
this.lastKey = _.last(movies)['$key']
const newMovies = _.slice(movies, 0, this.batch)

/// Get current movies in BehaviorSubject
const currentMovies = this.movies.getValue()

/// If data is identical, stop making queries
if (this.lastKey == _.last(newMovies)['$key']) {
this.finished = true
}

/// Concatenate new movies to current movies
this.movies.next( _.concat(currentMovies, newMovies) )
})
.take(1)
.subscribe()
}
}

Final Thoughts

You could improve on this implementation with an Angular list animation to stagger the appearance of new items as they are retrieved from the database.

Also, keep in mind that you lose the realtime connection to Firebase by creating your own BehaviorSubject. This is probably not an issue for most infinite scroll situations, but something to keep in mind.

That’s it infinite scroll. Let me know what you think in Slack or in the comments below.