Left ArrowBack

notes / JavaScript / Web API / intersection observer / infinite scroll

infinite scroll

A vertical list of squares

Infinite Scroll

Planted

Status: seed

An infinite scrolling page continually fetches more content as the user scrolls. Commonly used when the page's content is too big to handle in 1 request. For example, Pinterest's landing page. It displays a list of items that are paginated (grouped in batches of 40 for example). On page load, the 1st batch is displayed. As the user scrolls, but before they reach the bottom, a request is made to fetch the next batch. This continues until there are no more batches.

This approach to loading content removes the need for a user action to see the next batch. For example, clicking the Next link at the bottom of a Google search.

The word 'Goooooooooooogle. A list of numbers from 1 to 10 below it & a Next link.

1st Batch

To create an infinite scroll, we will pretend we have an endpoint that batches items in groups of 10. Items will be rendered on the page in a vertical list. The 1st batch will be hardcoded in HTML representing a server-side render. In case the user scrolls fast enough to reach the bottom of the list before the next batch of results has been fetched, an additional list item will be added with the text Loading....

An isometric view of the DOM. Showing the viewport, a list element & a list item with the text 'Loading...'

localhost:3000

Console

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
    <li>7</li>
    <li>8</li>
    <li>9</li>
    <li>10</li>
    <li id="loader">Loading...</li>
</ul>

Next Batch

To fetch more items, create an element we'll refer to as fetchTrigger within the <ul />. Set:

  • <ul /> to position: relative
  • fetchTrigger to position: absolute, bottom: 0. Making it anchored to the bottom of the list.
An isometric view of the DOM. Showing the viewport, a list element, a list item with the text 'Loading...' & a shaded element in front of it.

Observe fetchTrigger using the Intersection Observer, set the following in the options object:

  • omit the root property
  • threshold: 0

This sets the viewport as the root. When 1px of fetchTrigger intersects it, a callback will be executed. Create a function that fetches the next batch & adds it to the list. Call it when the observer is triggered & isIntersecting: true.

localhost:3000

Console

const fetchTrigger = document.querySelector("#fetchTrigger");
const ul = document.querySelector("ul");
const loader = document.querySelector("#loader");

const options = {
    rootMargin: "0px",
    threshold: 0,
}

let lastFetchedPage = 0;
let isFetching = false;

async function onIntersect() {
    isFetching = true;
    lastFetchedPage++;

    const data = await fetchData(lastFetchedPage);
    const lis = createLis(data);
    ul.insertBefore(lis, loader);

    isFetching = false;
}

function callback(entries, observer) {
    const { isIntersecting } = entries[0];
    if (isIntersecting && !isFetching) {
        onIntersect();
    }
}

const observer = new IntersectionObserver(callback, options);
observer.observe(fetchTrigger);

The height of fetchTrigger will determine how far the user has to be from the bottom of the page to trigger a fetch. Because it should not be seen or interacted with, set pointer-events: none & aria-hidden="true". In the example above, it has a background color for demo purposes only.

Where to Next?

A sci-fi robot taxi driver with no lower body