Infinite Scrolling: Mastering the Intersection Observer API By Making Seamless Infinite Scroll like a Pro
Tags: webdev, javascript, beginners, programming
Published: 2024-07-01 09:11:04


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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j0llkajamgmo03xw4hot.gif)

In this article, we will implement an Infinite Scrolling using Intersection Observer API in vanilla Javascript.`Infinite scrolling `is 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7svhwm9obmca3cv8yrsz.png)

**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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6jjcsjdmzg9ks9476y7i.jpeg)
**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**

```html
<!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>

```

2. **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**_

```javascript
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
```
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

```javascript
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);
    }
}
```
- Function that is going to append the data dynamically in our web page. we are going to keep it simple

```javascript
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);
    });
}
```
**Drum roll please...Now Here is Where the magic starts**

- Setting up the intersection observer

```javascript
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 });
```
Lets go through the above code step by step

First we are a creating a instance called `observer`

```javascript
const observer = new IntersectionObserver(async (entries) => {}, { threshold: 1.0 });
```
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

```javascript
observer.observe(loadingDiv);
```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/guqwh4m47ggouzsgqldr.png)

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
4. `intersectionRect:` Intersection area relative to the root
5.  `rootBounds:` Size of the root (viewport) / in our case the broswer window
6. `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` is` false` 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vega5tv4rp8bbmgo8llg.png)

**Finally we have Window Event Listener:**

```javascript
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);
    }
});
```
Executes when the DOM content is fully loaded. Calls getPosts to fetch initial posts (`currentPage = 1`) and appends it using appendData

**Full Code**

```javascript
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);
    }
});

```
**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!