Skip to content

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=1
https://www.reddit.com/r/${subreddit}/hot/.json?limit=100&page=2
https://www.reddit.com/r/${subreddit}/hot/.json?limit=100&page=3

We will instead request data after the name a specific post, like this:

https://www.reddit.com/r/${subreddit}/hot/.json?limit=100&after=somepost
https://www.reddit.com/r/${subreddit}/hot/.json?limit=100&after=someotherpost
https://www.reddit.com/r/${subreddit}/hot/.json?limit=100&after=anotherpost

We 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$.

//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.

Terminal window
npm install ngx-infinite-scroll
import { 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.