How to implement an infinite scroll using intersection observer API
Intro
This is a follow up to my previous article on how to build a collapsible sticky header with intersection observer. I'd recommend reading that first if you're unfamiliar with the Intersection Observer API.
I'll be showing you how to implement a simple infinite scroll example using the intersection observer API.
An in-depth video walkthrough is also available.
What is an infinite scroll?
Infinite scroll is a technique that allows users to scroll through a massive chunk of content with no finishing-line in sight. It is an alternative to pagination and load more button. There are a lot of resources out there advocating against the use of infinite scroll because they are often used for the wrong reasons.
However, it is not my job to judge your use case, I'm just here to show you how to implement one.
Setting up the environment
So the idea is very simple, we need to have an API that gives us more data when we scroll past a certain threshold on the page.When the threshold is triggered, we load more DOM elements based on that data.This threshold detection will be the sole responsbility of the intersection observer.
To replicate the API environment, I created an async function to mimic network latency of API requests, in which we need to wait for the data to come back. It takes in a paging object as a parameter to generate easily visualizable data for our DOM elements.
const fetchPagingAPI = function(paging) {const newData = new Array(paging.count).fill().map((_, idx) => paging.start + idx);return new Promise((resolve) => {setTimeout(() => resolve(newData), 1000);});}
In this example, we will have an infinite scroll container and a loader element that informs us when we are requesting for more data.
<ul id="infinite-scroll-list"><p id="loader">isLoading</p></ul>
Setting up the infinite scroll
Because an infinite scroll is heavily re-usable, I defined a InfiniteScroll
class.
The InfiniteScroll
class will take in the identifers for the infinite scroll container, loader, and also the API and its necessary metadata.
I also have defined the following workflow:
- The infinite scroll needs to start with some data populated.
- The loader should appear when we are loading more data and disappear when we are not loading.
- We specify an element to be the threshold for loading more data
To satisfy those workflows, we have the following functions:
_paginate
for when we need to load more data._showLoader
for showing and hiding the loader_paginateCallback
a callback wrapped around_paginate
for the intersection observer when the threshold is hit._addToDOM
, to generate the new elements to add to the DOM.
Initialization
On the initialization of the InfiniteScroll
, we need to internally track the current state of our pagination and also invoke the initial pagination call to populate our UI.
constructor(infiniteScrollId, loaderId, api, { count }) {this.infiniteScrollId = infiniteScrollId;this.loaderId = loaderId;this.fetchAPI = api;this._start = 0;this._count = count;this._showLoader(true);this._paginate().finally(() => {this._showLoader(false);});}
_paginate
For pagination, we need to asynchronously fetch the API, increment our internal pagination state and populate the DOM.
async _paginate() {const paging = {start: this._start,count: this._count}const data = await this.fetchAPI(paging);this._start += this._count;this._addToDOM(data);
_showLoader
This function takes care of showing and hiding the loader element with our show and hide css classes.
if (show) {this.loaderElement.classList.add('show');this.loaderElement.classList.remove('hide');} else {this.loaderElement.classList.remove('show');this.loaderElement.classList.add('hide');}
_addToDOM
This function is a bit more involved. To minimize the amount of DOM manipulations, we use a DocumentFragment to store all the new elements generated from the API data
and then append it to the infinite scroll element at the end. This way, we are only making the browser repaint once instead of the number of new elements.
_addToDOM(data) {const fragment = document.createDocumentFragment();for (let i = 0; i< data.length; i++) {const li = document.createElement('li');li.textContent = data[i] + 1;li.classList.add('item');fragment.appendChild(li);}this.infiniteScrollElement.appendChild(fragment);}
_paginateCallback
Here, we check for intersection. If the element is intersecting, that means the top of it is entering the viewport, so we should start paginating.
entries.forEach(entry => {if (entry.isIntersecting) {// show a loaderthis._showLoader(true);this._paginate().finally(() => {this._showLoader(false);});}})
I don't see the observer being used?
Good eye! The reason it is not being used is because depending on what you want to do, it can be implemented differently!
For our example, let's say we want to use the the top of the last element of the infinite scroll as the trigger for pagination.
All we have to do is modify the constructor
and _addToDOM()
like so:
Note: It is very important for us to bind to perserve the context of the InfiniteScroll
in the callback and also for us to unobserve so the old element don't get re-used as a trigger.
{constructor(infiniteScrollId, loaderId, api, { count }) {...this._observedElement = undefined;this._observer = new IntersectionObserver(this._paginateCallback.bind(this));...}_addToDOM(data) {...if (this._observedElement) {this._observer.unobserve(this._observedElement);}for (let i = 0; i< data.length; i++) {...if (i == data.length - 1) {// add observethis._observer.observe(li);this._observedElement = li;}...}...}}
Other things that you can do are adding intersection options and modifying _paginateCallback
.
Conclusion
Hopefully, this helps give you an idea of how to implement an infinite scroll using intersection observer. The idea shown here should be easily translatabe to other web frameworks, such as React, Angular, and Ember.
Like always, here is a full jsfiddle for you to play with. Enjoy!