DEV Community

Cover image for Futuristic Infinite Scrolling in React and JavaScript
Dhairya Nadapara
Dhairya Nadapara

Posted on

Futuristic Infinite Scrolling in React and JavaScript

There are multiple use cases in the modern UX when we are required to use infinite scrolling. Previously, devs are using the height of the viewport and element to find out the intersection of the element is in the viewport. The main issue in the same is that the function that calculations will be executed on the main queue so it makes your app a bit slow and it was bit unreliable. A few days ago I came across the Intersection Observer API. Which can be used in the following applications:

  • Lazy-loading of images or other content as a page is scrolled.
  • Implementing "infinite scrolling" websites, where more and more content is loaded and rendered as you scroll so that the user doesn't have to flip through pages.
  • Reporting of visibility of advertisements to calculate ad revenues.
  • Deciding whether or not to perform tasks or animation processes based on whether or not the user will see the result.

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.

Source code is available at https://github.com/dhairyanadapara/infinite-scoller-example

Demo link: https://dhairyanadapara.github.io/infinite-scoller-example/

Let's start with the solution.

import React, { Component } from "react";
import "./App.css";

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
        };
    }
    componentDidMount() {
        this.createObserver();
    }

    createObserver = () => {
        let options = {
            root: null,
            rootMargin: " 40px",
            threshold: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
        };
        const boxElement = document.getElementById("loading");
        const observer = new IntersectionObserver(
            this.handleIntersect,
            options
        );
        observer.observe(boxElement);
    };

    handleIntersect = (entries, observer) => {
        const { arr } = this.state;
        entries.forEach((entry) => {
            console.log(entry.intersectionRatio);
            if (entry.intersectionRatio > 0) {
                this.setState({
                    arr: arr.concat([
                        10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
                    ]),
                });
            }
        });
    };

    render() {
        const { arr } = this.state;
        return (
            <div className="App" id="app">
                <div id="infinite-container">
                    <div class="cards-list" id="card-list">
                        {arr.map((x) => (
                            <div class="card 1">
                                <div class="card_image">
                                    {" "}
                                    <img src="https://i.redd.it/b3esnz5ra34y.jpg" />
                                </div>
                                <div class="card_title title-white">
                                    <p>Card Title</p>
                                </div>
                            </div>
                        ))}
                    </div>
                    <div id="loading" style={{ height: "100px" }}>
                        Loading
                    </div>
                </div>
            </div>
        );
    }
}

export default App;
Enter fullscreen mode Exit fullscreen mode

As you can see we have used the react class component so it will be easy to understand. You can use functional components also.

Let's start with understanding the observer initialization.

createObserver = () => {
    let options = {
        root: null,
        rootMargin: " 40px",
        threshold: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
    };
    const boxElement = document.getElementById("loading");
    const observer = new IntersectionObserver(this.handleIntersect, options);
    observer.observe(boxElement);
};
Enter fullscreen mode Exit fullscreen mode

IntersectionObserver takes 2 arguments.

  1. options
    options are the configuration for Intersection Observer. It has 3 properties:

    1. root: The element which you want to use as the viewport. If you want to use the browser's viewport pass null.
    2. rootMargin : Offset is added to the target rectangle while calculating intersections
    3. threshold: A list of thresholds, sorted in increasing numeric order. The callback will be called when intersectionRatio passes the threshold
  2. callback
    callback has 2 argument:

    1. entries list of IntersectionObserverEntry, which describes the intersection between target and root element
    2. observer IntersectionObserver object same we have created in createObserver

Here we are observing the loading element which will be at bottom of the card list. In our case, we have only 1 target element in the observer so we will get only 1 object in entries. If you have multiple target elements targets to the same observers you will get more entries.

handleIntersect = (entries, observer) => {
    const { arr } = this.state;
    entries.forEach((entry) => {
        if (entry.intersectionRatio > 0) {
            this.setState({
                arr: arr.concat([
                    10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
                ]),
            });
        }
    });
};
Enter fullscreen mode Exit fullscreen mode

IntersectionObserverEntry object have multiple attributes like boundingClientRect,intersectionRatio,intersectionRect,isIntersecting,rootBounds,target,
time.

The main attributes are:

  • intersectionRatio: returns the percentage of intersectionRect to the boundingClientRect
  • isIntersecting: return if target and root are intersecting or not.
  • target: this is an important attribute when we have multiple targets attached to the same observer

In the above function we have iterated over the entries and checked if the intersection ratio is more than 0 is not means the target element has an intersection with root or viewport is happened or not. As you can see we are observing the element with id loading which is placed at bottom of the card-list element. So what will happen when we scroll down and reach the loading element it intersection will happen and the state will be updated accordingly.

In this case, we are not doing any API calls so data is getting updated quickly. In case of fetch request, it would be better to use the rootMargin. You can also update the threshold based on requirements.

Discussion (13)

Collapse
joolucas_silveirasilva profile image
João Lucas Silveira Silva

We are in 2021, please stop writing class components

Collapse
ortonomy profile image
🅖🅡🅔🅖🅞🅡🅨 🅞🅡🅣🅞🅝

Lol, right? Doesn’t really lend weight to the hyperbolic title of ‘futuristic’ now, does it?

Collapse
vndre profile image
Andre

this.state = {
arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
};

My heart omg 😱

Also the IntersectObserver API was introduced on chrome since 2016...

Collapse
dhairyanadapara profile image
Dhairya Nadapara Author • Edited

Agree with you @vndre it been there for a long time. But people still doesn't know about this. I have searched for infinite scrolling react component on npm and haven't found any which is community maintained. Thanks for suggestion, I will try to improve quality of my sample code to be more realistic with api call. Thanks again :)

Collapse
vndre profile image
Andre

What about github.com/thebuilder/react-inters... ?

I believe a hook approach would make things easier.

Thread Thread
dhairyanadapara profile image
Dhairya Nadapara Author

Yes, I have seen the react-infinite-intersection but, I was confused because they have very few download on npm.

There are two reasons why I'm more inclined towards the class components:

  1. I'm more inclined towards the object oriented programming. So, I get more comfortable with class components and they are easy to understand for me
  2. Some times before I have to add some feature in a functional components and it was developed by someone in organization. At the beginning there was no issue but as the component complexity increases the issues were getting worse. It's easy to write but it's easy to make mistakes too.

For creating small components like UI framework with independent components or stateless components, I use functional components but for components which are going be complex with lots of features, I prefer class components.

Thanks :)

Collapse
vetras profile image
vetras

The div id="loading" never leaves the page. It is just pushed further down.

This is weird ... but OK I guess the main point is using the
IntersectionObserver and looking at the value of the intersectionRatio to make decisions.

That same principle can be applied to other things as you mention at the top. Cool.

Collapse
dhairyanadapara profile image
Dhairya Nadapara Author • Edited

Yes @vetras , usually web-app's shows the loading at bottom of the div in case of infinite scrolling. Other way I thought of was set target to last element of the list, but in that case we have to change the observer target when list updates. If you know any other way please share, it will be helpful. Thanks :)

Collapse
vetras profile image
vetras

What I usually see when we have DOM elements we want to show or hide dynamically is to have a display property on them (not just shove them outside the view 🙏 ).

For example:

  • on vue.js:
<p v-if=isLoading > loading... please wait </p>
Enter fullscreen mode Exit fullscreen mode
  • on react:
render() {
  return this.isLoading && (<p>loading ... please wait</p>);
Enter fullscreen mode Exit fullscreen mode

(full stack dev here, FYI, so my FE well...)

Thread Thread
dhairyanadapara profile image
Dhairya Nadapara Author

Oh yes, I completely missed this. Let me think of this how it will be achieved as target element is required to pass the in observe method.

Collapse
rexgalilae profile image
RexGalilae

This article doesn't make any attempts to be easy to understand.

It uses class components to try doing that even if it achieves the opposite and the explanations are very lackluster at best.

Just a glorified code dump

Collapse
Sloan, the sloth mascot
Comment deleted
Collapse
iskurbanov profile image
iskurbanov

Don't have anything nice to say? Don't say it :)