Intersection observer and infinite scroll in a visual way
Tags: javascript, webdev, tutorial
Published: 2022-06-03 12:23:12

Infinite scroll in a visual way

{% embed https://www.youtube.com/embed/fvU6MC1cK9U %}
_See this and many other articles at [lucaspaganini.com](lucaspaganini.com)_

## Short Version

Most implementations of infinite scroll involve listening to the `scroll` event. But that triggers too many times, and you also need to make some calculations to check if the user is at the bottom of the page.

```ts
const onWindowScroll = () => {
  console.log('scroll');
  const reachedBottom = isAtTheBottom();
  if (reachedBottom) getMoreItems();
};
```

You could use the throttle or debounce to optimize it, but it's still not ideal.

A better solution is to put an element at the bottom of the page and use the `IntersectionObserver` to be notified when the element enters the view.

If that short version was enough for you, just leave a like and have a great day! Heck, you could even drop a [tweet](https://twitter.com/LucasPaganini), if you want me to have a great day.

If you stick around, I'll show you an infinite scroll implementation using scroll events, then we'll optimize its performance with throttling and debouncing, and in the end, I will show you how to make it even better using the `IntersectionObserver`.

## Introduction

This is the implementation using scroll events, here's how it works:

We have a `getItems(amount, pointer)` function that returns a promise of an array of items to show. We can specify the number of items we want to get, and we can also give it a `pointer`, which is just a number indicating the last item we got.

```ts
export const getItems = async (
  amount: number,
  pointer: number = 0
): Promise<{ items: Array<Item>; pointer: number; hasMore: boolean }> => {
  // Wait a second to simulate delay
  await new Promise((res) => setTimeout(res, 1000));

  // Get items
  const itemIndexes =
  const items = Array.from({ length: amount })
    .map((_, i) => i + pointer)
    .map((index) => ITEMS[index] ?? null)
    .filter((v) => v !== null);

  const lastPointer = items.length + pointer;
  const hasMore = lastPointer < ITEMS.length - 1;

  return { items, pointer: lastPointer, hasMore };
};
```

When we receive the items, we create a card for each and request more items when the user scrolls to the bottom of the cards list.

To see if the user is at the bottom, we listen to `scroll` events in the document, and then we run a function called `isAtTheBottom()` which returns `true` if the user is at the bottom.

```ts
window.addEventListener('scroll', () => {
  const reachedBottom = isAtTheBottom();
  if (reachedBottom) {
    // Get the items
    const items = getItems(...);
    // Render a card for each item
    renderCards(items);
  }
})
```

`isAtTheBottom()` is the only relevant function so far, the rest of the implementation is just styling, rendering the cards, and a loading spinner. But if you're curious, I'll leave a link for the repository in the references.

```ts
export const isAtTheBottom = (margin = 0): boolean => {
  const viewportHeight = window.innerHeight;
  const pageHeight = window.document.body.scrollHeight;
  const pageScrollY = window.scrollY;

  const pixelsToReachBottom = pageHeight - (pageScrollY + viewportHeight);
  return pixelsToReachBottom - margin <= 0;
};
```

## The Problem with Scroll Events

As I've said in the beginning, scroll events trigger too much. And we call `isAtTheBottom()` every time a `scroll` event triggers.

Here, I'll scroll to the bottom of the page and log every time a scroll event triggers. Look how many they are. `{ amount }` events just now.

I already gave you a spoiler, you know we're going to ditch the scroll events. But before doing that, I'll show two optimizations we could make to drastically improve the performance while still listening to scroll events.

## Throttling

The first one is throttling.

Define a duration, say 100 milliseconds for example.

Throttling the scroll events by 100 milliseconds means letting a scroll event pass, then ignoring all other scroll events for the next 100 milliseconds.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v9d8a16jsjygjcrf8zkt.png) 

To use throttling we can create a higher-order function.

<p>To summarize, in case you don't know what that is, a higher-order function is a function that receives a function and returns another function. It can be hard to understand at first, I'm writing a book about functional programming and this is one of the topics I'll be covering in the book. Quick plug, you can <app-newsletter-modal-trigger>sign up for our newsletter</app-newsletter-modal-trigger> to be notified when the book is done.</p>

```ts
// Example of a higher order function
export const logDuration =
  (fn) =>
  (...args) => {
    console.log('start', Date.now());
    fn(...args);
    console.log('end', Date.now());
  };
```

Anyway, back to throttling.

A simple implementation of throttling in javascript could be the following:

```ts
const throttle = <F extends (...args: Array<any>) => void>(
  fn: F,
  duration: number
): ((...args: Parameters<F>) => void) => {
  let waiting = false; // Initially, we're not waiting
  return function (...args: Parameters<F>): void {
    // We return a throttled function
    if (waiting === false) {
      // If we're not waiting
      fn.apply(this, args); // Execute users function
      waiting = true; // Prevent future invocations
      setTimeout(() => {
        // After a period of time
        waiting = false; // And allow future invocations
      }, duration);
    }
  };
};
```

That implementation is **very** simplified. I do not recommend using it. Lodash has a great `throttle` implementation that is battle-tested and much more complete. Use Lodash.

## Debouncing

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sx8tabqxxn291j3s2gup.png) 

Lodash also has `debounce`, which is similar to `throttle`. They are so similar that if you enable some options in Lodash's `debounce` implementation, you get `throttle`.

Since you can use options to make them behave the same way, the differences I'm about to present are considering `debounce` with no options, Only it's default behavior.

Debouncing the scroll events by 100 milliseconds means only letting a scroll event pass after 100 milliseconds has passed without scroll events.

Think of them like you're swimming in a pool and every light bulb is an event. If you're throttling, you get to breathe once in a while. But if you're debouncing, you only get to breathe after a long enough space with no light bulbs.

So, which one is better? Throttling or debouncing? It depends on your use case. If you're indeed swimming, I recommend throttling.

I won't go deeper into those two techniques, **but** I'll leave a link in the references for [David Corbacho's article on CSS Tricks](https://css-tricks.com/debouncing-throttling-explained-examples/) where he does a great job explaining how they work and their use cases. Go, David.

## Reinterpreting the Algorithm

So far, we've been listening to scroll events and checking if the user is at the bottom of the page.

Now, let me ask you a question: do we care about scroll events? No.

We only care about whether the user is at the bottom of the page. Scroll events are just a warning that the user has changed their position. We could ignore scroll events altogether and run our check every second, it would work just fine.

So, instead of listening to scroll events, or an interval, or whatever else. Maybe we can be more direct. What if we could listen to when the user reaches the bottom of the page? That's all we want to know, right?

And we can do that using the `IntersectionObserver`.

## Intersection Observer

The `IntersectionObserver` observes intersections between elements and the viewport.

Imagine that you're on a page. The viewport is the part of the page that is currently visible. As you scroll up, the viewport goes up. As you scroll down, the viewport goes down.

Now, suppose you have an element below your viewport. As you scroll down, that element will intersect with the viewport, triggering the `IntersectionObserver`.

By default, the `IntersectionObserver` triggers as soon as even one pixel of your element is visible. You can change that behavior with the `threshold` option. You can tell it to trigger only when 100% of the element is visible or to trigger every 10%, or every 25%, you name it.

For our purposes, we want to know when the user reaches the bottom of the page. So we will add an element to the bottom of the page and listen to intersections on that element. As soon as 1 pixel of that element becomes visible, we request more items.

```ts
const intersectionObserver = new IntersectionObserver((entries) => {
  const isIntersecting = entries[0]?.isIntersecting ?? false;
  if (isIntersecting) this._getMoreItems();
});

const bottomElement = document.querySelector(...);
intersectionObserver.observe(bottomElement);
```

That's way more performant and declarative than using scroll events.

## Intersection Margin

There’s another thing I like to do when I’m implementing infinite scroll. Instead of requesting more items when the user reaches the bottom, I like to request more when the user is close enough to the bottom. 100 pixels from the bottom, for example.

To do that, we can add a top margin of 100 pixels to our intersection observer using the `rootMargin` option.

```ts
const intersectionObserver = new IntersectionObserver((entries) => {
  const isIntersecting = entries[0]?.isIntersecting ?? false;
  if (isIntersecting) this._getMoreItems();
}, { rootMargin: "100px 0 0 0" });

const bottomElement = document.querySelector(...);
intersectionObserver.observe(bottomElement);
```

## Browser Support

Regarding browser support, the `IntersectionObserver` is available in all major browsers, and you can [use a polyfill](https://www.npmjs.com/package/intersection-observer) if you need to support older browsers.

## Intersections with Other Elements

There’s one more option to customize the `IntersectionObserver`. We don’t need to use that option for infinite scroll, but I’ll explain it to you for completeness.

Suppose you have a scrollable element, and you want to listen to intersections between that element and one of its child elements. Got it? In that case, you don't want to observe intersections between an element and the viewport, you want to observe intersections between two elements. You can do that using the `root` option.

```ts
const outerElement = document.querySelector(...);
const intersectionObserver =
  new IntersectionObserver(callback, { root: outerElement });

const innerElement = document.querySelector(...);
intersectionObserver.observe(innerElement);
```

One restriction here is that the root element needs to be a parent of the elements you want to observe.

If you don’t specify the `root` (or set it to `null`), it will use the viewport.

## Angular Directive

If you read our articles, you know that we use Angular a lot. To make our lives easier, we’ve created a directive to listen to intersection events.

It's available in our Angular utilities library, just `import { IntersectionObserverModule } from '@lucaspaganini/angular-utils'` and use the `(lpIntersection)=“onIntersection($event)”` directive in your templates. You can also pass intersection options with the `[lpIntersectionOptions]=“options”` parameter.

```ts
import { IntersectionObserverModule } from '@lucaspaganini/angular-utils'

@NgModule({
  ...
  imports: [ ..., IntersectionObserverModule, ... ],
  ...
})
```

```ts
<div
  (lpIntersection)="onIntersection($event)"
  [lpIntersectionOptions]="options"></div>
```

## Conclusion

I’ll leave a link for the repository in the references.

As I was writing this article, I started to wonder if there’s a way to implement infinite scroll without adding an element to the end of the list. If you have a solution for that or anything else to contribute, please, send a [tweet](https://twitter.com/LucasPaganini).

You can also hire us. We're not an agency, we're a team, and I'm personally responsible for every project. We have a limited amount of clients because I'm human, and I need to sleep. But right now, we're available, so go to [lucaspaganini.com](https://lucaspaganini.com) and tell me, how can we help you?

Have a great day and I'll see you soon

## References

1. [Repository _GitHub_ ](https://github.com/LucasPaganini/published-content/tree/master/infinite-scroll-intersection-observer)
2. [Debouncing and Throttling Explained by David Corbacho](https://css-tricks.com/debouncing-throttling-explained-examples/)
3. [Intersection Observer _Mozilla docs_](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
4. [Throttle _Lodash docs_ ](https://lodash.com/docs/4.17.15#throttle)
5. [Intersection Observer Polyfill _npm_](https://www.npmjs.com/package/intersection-observer)
6. [Angular Utilities Library _npm_](https://www.npmjs.com/package/@lucaspaganini/angular-utils)