Adding Pagination with Infinite Scroll
At the moment, we just get a bunch of GIFs, display them, and that’s it. We are going to improve this a little now. Instead of just dumping everything we get from a single request, we are going to only display a set amount of GIFs per “page”. When the user scrolls to the bottom of the page, we will automatically load in more GIFs if they are available.
To do this, we will need to make some changes to our gifsLoaded$ stream, and
we are also going to make use of the third party ngx-infinite-scroll library
to help us trigger a load when the user scrolls to the bottom of the page.
Adding Support for Pagination
Before we can trigger a new page with our infinite scroll mechanism, we need a way to actually trigger and load new pages.
We will do this by extending our RedditService with a pagination$ source. We
are going to have to make some other changes as well.
When we are loading data from the Reddit API we do not just load specific pages of data, e.g:
https://www.reddit.com/r/${subreddit}/hot/.json?limit=100&page=1https://www.reddit.com/r/${subreddit}/hot/.json?limit=100&page=2https://www.reddit.com/r/${subreddit}/hot/.json?limit=100&page=3We will instead request data after the name a specific post, like this:
https://www.reddit.com/r/${subreddit}/hot/.json?limit=100&after=someposthttps://www.reddit.com/r/${subreddit}/hot/.json?limit=100&after=someotherposthttps://www.reddit.com/r/${subreddit}/hot/.json?limit=100&after=anotherpostWe will need to keep track of the name of whatever the last known gif we
have seen is — as in the gif at the very end of our list. We will then use the
name of that gif as the after when we are trying to load a new page.
This means we are going to need to make some changes to our state. In fact, there are other things we will eventually want to store in the state too, so let’s handle all of that now.
export interface GifsState { gifs: Gif[]; error: string | null; loading: boolean; lastKnownGif: string | null;} private state = signal<GifsState>({ gifs: [], error: null, loading: true, lastKnownGif: null, });Let’s also add some more selectors to select our new state.
// selectors gifs = computed(() => this.state().gifs); error = computed(() => this.state().error); loading = computed(() => this.state().loading); lastKnownGif = computed(() => this.state().lastKnownGif);The next thing we will need is a new source that we can next when we want to
trigger a new page loading — we will call this pagination$. We will also need
to update our gifsLoaded$ source to react to the pagination$ source
emitting, and whenever it does it should trigger a new request to fetch data
from Reddit.
For now, we will just repeat the same request rather than worrying about
tracking the lastKnownGif. Just to get the basic set up working.
This is by no means an easy task, but see if you can figure out how to add the
pagination$ source and cause the gifsLoaded$ to emit with the data from an
HTTP request to the Reddit API every time that we call next on pagination$.
Click here to reveal solution
Solution
//sources pagination$ = new Subject<void>(); private gifsLoaded$ = this.pagination$.pipe( concatMap(() => this.fetchFromReddit('gifs')) );Now rather than gifsLoaded$ immediately making a single request to Reddit, it
will make one every time that pagination$ emits. If we used switchMap here
it would cause the currently executing request to be cancelled and we would
switch to the new request every time that pagination$ emits. This is why we
use concatMap instead. If the user were to trigger two page loads in quick
succession, we wouldn’t want the first request to be cancelled. This way, the
concatMap would wait for the first request to finish, and then it would start
the second request.
The problem here though is that now no gifs will load until we manually trigger the pagination source. That’s a bit annoying and doesn’t feel very reactive/declarative.
Instead, let’s add the startWith operator:
pagination$ = new Subject<void>(); private gifsLoaded$ = this.pagination$.pipe( startWith(undefined), concatMap(() => this.fetchFromReddit('gifs')) );This will cause the pagination$ stream to emit once immediately, which will
trigger our initial request, and then it will be triggered again every time that
we next the pagination$ source.
Now we need to deal with the situation of actually loading different data,
not just repeating the same request. This means that now we are going to need to
keep track of that lastKnownGif value.
To accomodate this, we will first update our fetchFromReddit to accept an
after parameter and to also return us the lastKnownGif from a particular
request.
private fetchFromReddit( subreddit: string, after: string | null, gifsRequired: number ) { return this.http .get<RedditResponse>( `https://www.reddit.com/r/${subreddit}/hot/.json?limit=100` + (after ? `&after=${after}` : '') ) .pipe( catchError((err) => EMPTY), map((response) => { const posts = response.data.children; const lastKnownGif = posts.length ? posts[posts.length - 1].data.name : null;
return { gifs: this.convertRedditPostsToGifs(posts), gifsRequired, lastKnownGif, }; }) ); }We now map our response and instead of just returning the gifs directly, we
return an object that contains those gifs, but it also contains two other
properties: our lastKnownGif and gifsRequired. We don’t actually need this
gifsRequired just yet, we will be using that later to keep track of whether we
have fetched enough gifs from a request or not.
Notice that we are now supplying the after parameter to the HTTP request
— initially this will be null so we will not append it, but once we have
a lastKnownGif supplied it will use that as the after.
Now we will also need to update our gifsLoaded$ reducer to keep track of the
last gif.
this.gifsLoaded$.pipe(takeUntilDestroyed()).subscribe((response) => this.state.update((state) => ({ ...state, gifs: [...state.gifs, ...response.gifs], loading: false, lastKnownGif: response.lastKnownGif, })) );Since we have modified the way our fetchFromReddit works, we will also need to
update our source.
pagination$ = new Subject<string | null>(); private gifsLoaded$ = this.pagination$.pipe( startWith(null), concatMap((lastKnownGif) => this.fetchFromReddit('gifs', lastKnownGif, 20)) );We now supply fetchFromReddit with the lastKnownGif that is stored in the
state, as well as how many gifs we want to fetch per page (we are not actually
doing anything with that yet).
Rather than directly accessing our this.lastKnownGif() state from within this
stream, we will instead have our pagination$ source pass in the value (as in
we will next our pagination source using the current lastKnownGif value).
Technically, this isn’t required, but it can keep things a bit cleaner and more organised if we don’t have our streams reaching out into other parts of our state for values in the middle of streams. This way, the value is provided directly to the stream from the beginning.
Now we should be able to trigger the next page of data loading in just by
calling pagination$.next() or pagination$.next(lastKnownGif). We don’t
currently have a way to trigger that though, so let’s work on that now.
Adding Infinite Scroll
First, we will need to install the ngx-infinite-scroll library.
npm install ngx-infinite-scrollimport { Component, inject } from '@angular/core';import { GifListComponent } from './ui/gif-list.component';import { RedditService } from '../shared/data-access/reddit.service';import { InfiniteScrollDirective } from 'ngx-infinite-scroll';
@Component({ selector: 'app-home', template: ` <app-gif-list [gifs]="redditService.gifs()" class="grid-container" /> `, imports: [GifListComponent, InfiniteScrollDirective],})export default class HomeComponent { redditService = inject(RedditService);} <app-gif-list [gifs]="redditService.gifs()" infiniteScroll (scrolled)="redditService.pagination$.next(redditService.lastKnownGif())" class="grid-container" />We add infiniteScroll to the element, which will attach the directive from the
library to this element, which will add this scrolled event which we can
utilise. When the scrolled event emits, we next our pagination$ source
(passing in the current lastKnownGif). This event will be triggered when we
are near the bottom of our list — you can configure this behaviour more
precisely if you look at the documentation of the library.
Now if you scroll to the bottom of the list — which is kind of hard to do at the moment because we are displaying way more gifs then there should be — you will see the next batch of gifs load in.
Pretty cool, right?
We will continue building on this functionality in the next lesson, where we will add the ability for the user to change the subreddit gifs are pulled from.