DEV Community

Cover image for Infinite Scrolling: Mastering the Intersection Observer API By Making Seamless Infinite Scroll like a Pro
John Muriithi
John Muriithi

Posted on

Infinite Scrolling: Mastering the Intersection Observer API By Making Seamless Infinite Scroll like a Pro

The Intersection Observer API provides a way to asynchronously observe changes in the intersection (visibility) of a target element with an ancestor element or the top-level document’s viewport. It is commonly used in implementing infinite scroll, triggering animations when an element enters the viewport, lazy-loading images etc.

It simplifies the process of triggering actions when elements become visible, eliminating the need for manual event handling and frequent polling.
Image description

In this article, we will implement an Infinite Scrolling using Intersection Observer API in vanilla Javascript.Infinite scrollingis an interaction design pattern in which a page loads content as the user scrolls down, allowing the user to explore a large amount of content with no distinct end.( I'm going to demonstrate how to use IntersectionObserver to implement infinite scrolling by fetching new content when a target element becomes visible (within our browser window))

We want the moment target element in our case Loading More Items as shown below becomes fully visible 100% in our browser window, then we fetch posts, think of it as essentially "sensing" or detecting when the user has scrolled to the bottom of the current content. This detection is facilitated by the IntersectionObserver API, which observes a target element at the bottom of the content (in our case). When this target element intersects with the viewport (i.e., comes into view, becomes fully visible like in the picture below), it triggers a callback function to load more content ( fetch more posts )

Image description

Lets first famirialize ourselves with Intersection Observer API

1. Create a IntersectionObserver - this observer will watch elements for us
let observer = new IntersectionObserver(callbackFunc, options)

callbackFunc - this callback will be called when the target element intersects with the device's viewport or the specified element

In other words, when the target element becomes visible or when it's visibility changes, this function will be called. when we talk of the target element "intersecting with the viewport" we mean when the part or all of that element becomes visible within the user's browser window.

options - let's you control the circumstances under which the observer's callback callbackFunc is invoked by defining a threshold. It's like setting instruction rules to your observer to keep watch of your target element until maybe the target element becomes fully visible, half visible or even an inch visible, then the observer reports to you by basically calling the callbackFunc. You can afterwards do something either with that target element etc

const options = { root: null, threshold: 0.5 };

threshold: 0.5 we say the callback will triggered when 50% of the target element is visible.
Therefore The observer’s callback function gets triggered when the intersection conditions (defined by the threshold) are met.

By default root is null, here we are passing it explicitly, but failure to include the property root it will be implicitly be set to null... NB you can set the root to be any element

2. Observer - we use observe method to start monitoring / observing a target element.You can have one or more target elements

observer.observe(targetElement);

observer: An instance of IntersectionObserver.
targetElement: The DOM element you want to observe for visibility changes.

The observe method is used to start observing a specific target element. When called, it tells the IntersectionObserver instance to track the specified element and report back whenever it becomes visible or invisible within the viewport or a defined root element.

Image description
Lets Wire Everything Up with a Clear Example

We will implement a simple web app that loads Posts dynamically as the user scrolls

  1. Define a Simple Html Structure
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Infinite Scroll Example</title>
    <style>
        #content {
            max-width: 600px;
            margin: 0 auto;
        }
        .item {
            padding: 20px;
            border: 1px solid #ccc;
            margin: 10px 0;
        }
    </style>
</head>
<body>
    <div id="content">
        <!-- Posts will be dynamically loaded here -->
    </div>

    <!-- This will be our target element -->
    <div id="loading" style="text-align: center; margin: 20px; background-color: rgb(173, 160, 185);">
        Loading more items...
    </div>
    <script src="InfiniteScroll.js"></script>
</body>
</html>

Enter fullscreen mode Exit fullscreen mode
  1. Create a Javascrit file and call it InfiniteScroll.js

Lets initialize some Important Variables here at the top
please take note of the inline documentation for explanation

let currentPage = 1; // this will help us keep track of the current page of data
let loading = false; // this will prevent multiple fetches of data at the same time
const contentDiv = document.getElementById('content'); // our post will be dynamically loaded here
const loadingDiv = document.getElementById('loading'); // this will be our target element
Enter fullscreen mode Exit fullscreen mode

We are going to define our function that is going to be responsible of fetching the post

We will use https://jsonplaceholder.typicode.com/posts?_limit=10&_page=${page} API endpoint.

Lets break it down, we have the base URL https://jsonplaceholder.typicode.com/posts this points to the 'post' resource on the JSONPlaceholder API and it provides data related to posts.

We have also have query parameters ?_limit=10&_page=${page} which filters the request
_limit=10 this tells the API to limit the response to 10 posts
_page=${page} this specifies which page of data you want to retrieve

For example, _page=1 would get the first set of 10 posts, _page=2 would get the next 10 posts, and so on. we will change the page variable dynamically

hence our function will look like this

const getPosts = async (page) => {
    try {
        let response = await fetch(`https://jsonplaceholder.typicode.com/posts?_limit=10&_page=${page}`);
        if (!response.ok) {
            throw new Error("HTTP error! Status: " + response.status);
        }
        return await response.json();
    } catch (e) {
        throw new Error("Failed to fetch services: " + e.message);
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Function that is going to append the data dynamically in our web page. we are going to keep it simple
const appendData = (data) => {
    data.forEach(item => {
        const div = document.createElement('div');
        div.className = 'item';
        div.innerHTML = `<h3>${item.title}</h3><p>${item.body}</p>`;
        contentDiv.appendChild(div);
    });
}
Enter fullscreen mode Exit fullscreen mode

Drum roll please...Now Here is Where the magic starts

  • Setting up the intersection observer
const observer = new IntersectionObserver(async (entries) => {
    if (entries[0].isIntersecting && !loading) {

        console.log(entries)

        loading = true;
        currentPage++;
        try {
            const data = await getPosts(currentPage);
            appendData(data);
        } catch (e) {
            console.log(e.message);
        }
        loading = false;
    }
}, { threshold: 1.0 });
Enter fullscreen mode Exit fullscreen mode

Lets go through the above code step by step

First we are a creating a instance called observer

const observer = new IntersectionObserver(async (entries) => {}, { threshold: 1.0 });
Enter fullscreen mode Exit fullscreen mode

We are passing two arguments callbackFunc and the options.

If you only pass the threshold option like { threshold: 1.0 } when creating an IntersectionObserver, it means that the root is implicitly set to its default value, which is null. In this case, the observer will use the browser's viewport as the root for detecting visibility. 1.0 means when the target is 100% visible, call the callbackFUnc immeadiatly.

The callback takes 2 arguments, the entries and the observer which is the InstersectionObserver itself. In our case we are passing only the entries argument.

What is this entries??, this is An array of IntersectionObserverEntry objects. Each entry represents an observed element / target element and contains information about its intersection with the root / or any specified element.

Let us first start observing our target element then we go back to the above explanation, it will make sense

observer.observe(loadingDiv);
Enter fullscreen mode Exit fullscreen mode

Here we are using observe method which when called, it tells the IntersectionObserver instance to track the specified element and report back whenever it becomes visible 100% or 1.0 as we set the threshold to that.

When the visibility of the target element changes according to the specified threshold, the callback function is triggered with an array of IntersectionObserverEntry objects and we are receiving them as entries, which provide details about the intersection.

Going back to the callbackfunction above, when we console.log(entries) we have something as below. You can clearly see that the entry represents an observed element (target element) and contains information about its intersection with the root.Since we are observing one target element, our entries array contains only one IntersectionObserverEntry object.

Image description

  1. We have time: This is Timestamp indicating when the intersection occurred / when the target element became fully visible in the browser window
  2. We have target: This is The observed element itself. Note that now you have freedom to manipulate this element like chaning its color etc 3.boundingClientRect: Position and size of the observed element
  3. intersectionRect: Intersection area relative to the root
  4. rootBounds: Size of the root (viewport) / in our case the broswer window
  5. isIntersecting: A boolean indicating whether the observed element is currently intersecting (whether it is visible) with the root (or viewport) according to the specified threshold

Then inside the callback function, this is what we are doing;

(entries[0].isIntersecting && !loading): Checking if the first entry (entries[0]) is intersecting with the viewport (isIntersecting is true) and ensures that loading isfalse to prevent multiple concurrent posts fetches.

loading = true: we are Seting the loading flag to true to indicate that a data fetch is in progress.

currentPage++: Is Incrementing the currentPage variable to fetch the next page of data.

const data = await getServices(currentPage): we are calling the asynchronous getPosts function to fetch Posts for the incremented currentPage.

appendData(data): Appends the fetched posts (data) to the DOM using the appendData function.

loading = false: we are reseting the loading flag to false after posts fetching and appending are completed. This allows subsequent fetches to occur when the loadingDiv intersects (becomes fully visible) with the viewport again.

Lets monitor the progress of our infinite scroll implementation and see how requests are being made using the Developer Tools in our browser

Image description

Finally we have Window Event Listener:

window.addEventListener('DOMContentLoaded', async () => {
    try {
        const posts = await getPosts(currentPage);
        if (posts) {
            appendData(posts);
        } else {
            console.log('posts not found or undefined');
        }
    } catch (e) {
        console.log(e.message);
    }
});
Enter fullscreen mode Exit fullscreen mode

Executes when the DOM content is fully loaded. Calls getPosts to fetch initial posts (currentPage = 1) and appends it using appendData

Full Code

let currentPage = 1; // this will help us keep track of the current page of data
let loading = false; // this will prevent multiple fetches of data at the same time
const contentDiv = document.getElementById('content'); // our post will be dynamically loaded here
const loadingDiv = document.getElementById('loading'); // this will be our target element

const getPosts = async (page) => {
    try {
        let response = await fetch(`https://jsonplaceholder.typicode.com/posts?_limit=10&_page=${page}`);
        if (!response.ok) {
            throw new Error("HTTP error! Status: " + response.status);
        }
        return await response.json();
    } catch (e) {
        throw new Error("Failed to fetch services: " + e.message);
    }
}

const appendData = (data) => {
    data.forEach(item => {
        const div = document.createElement('div');
        div.className = 'item';
        div.innerHTML = `<h3>${item.title}</h3><p>${item.body}</p>`;
        contentDiv.appendChild(div);
    });
}

const observer = new IntersectionObserver(async (entries) => {
    if (entries[0].isIntersecting && !loading) {

        console.log(entries)

        loading = true;
        currentPage++;
        try {
            const data = await getPosts(currentPage);
            appendData(data);
        } catch (e) {
            console.log(e.message);
        }
        loading = false;
    }
}, { threshold: 1.0 });

observer.observe(loadingDiv);

window.addEventListener('DOMContentLoaded', async () => {
    try {
        const posts = await getPosts(currentPage);
        if (posts) {
            appendData(posts);
        } else {
            console.log('posts not found or undefined');
        }
    } catch (e) {
        console.log(e.message);
    }
});

Enter fullscreen mode Exit fullscreen mode

Final Thoughts

In this article, we've explored the powerful IntersectionObserver API and its role in creating seamless infinite scrolling experiences. By leveraging this API, you can enhance the usability of your web applications, making them more dynamic and engaging for users. Here's a recap of what we covered

Key Concepts:

IntersectionObserver API:

Provides an efficient way to monitor the visibility of target elements within the viewport.

Simplifies the process of triggering actions when elements become visible, eliminating the need for manual event handling and frequent polling.

Infinite Scrolling:

Allows for continuous content loading as the user scrolls, improving the browsing experience by providing a steady stream of new content without requiring page reloads or manual pagination.

Callback Functions and Options:

The callbackFunc is called when the target element's visibility changes based on specified threshold values.
The options object allows fine-tuning of the observer's behavior, such as setting the root and threshold.

Conclusion

By mastering the IntersectionObserver API, you can create sophisticated and user-friendly web applications that offer a seamless and interactive experience. Whether you're building a news feed, an e-commerce site, or any other content-heavy application, infinite scrolling powered by IntersectionObserver ensures that users stay engaged and your app performs efficiently.

So go ahead, dive into your code editor, and start implementing infinite scrolling in your projects today. Your users will thank you for the smooth and continuous browsing experience!

Top comments (4)

Collapse
 
henrywoody profile image
Henry Woody

Good post. The rootMargin option is a good one to know. It's useful if you want to start loading the next batch of results a bit optimistically before the end of the current content is in view, which works if you supply a negative vertical value (e.g. to start loading when the end is 300px from the bottom of the window).

Collapse
 
john_muriithi_swe profile image
John Muriithi

Good Point Sir,,It's also worth noting that failure to include it, it defaults to 0px 0px 0px 0px

Collapse
 
cre8stevedev profile image
Stephen Omoregie

Interesting post! The intersection observer is a great browser api in JavaScript.

Collapse
 
john_muriithi_swe profile image
John Muriithi

sure bro,,Js will remain to be king of the Web,,look forwad for posts like this and many more