DEV Community

Cover image for Creating infinitely scrolling SPA using React
Arunabh Arjun
Arunabh Arjun

Posted on

Creating infinitely scrolling SPA using React

Introduction

Before starting with this blog, check out this to get the best idea of exactly what we are trying to achieve -

https://articles-app.arunabharjun.vercel.app/

So you have experienced infinite scrolling in apps like instagram, facebook, linkedIn, etc. wherein as soon as we reach the bottom of the page, more data loads, unless obviously there is no more data to load. And that feature is really kinda cool, isn't it? And you would like to implement that in your web-app too but have been wondering how to achieve that with your React.js app. We all love and adore how React.js simplifies dynamic client side web development for us, and we all are familiar with the common hooks like useEffect & useState, but React.js has a lot more under the hood, and today we are going to explore another hook which is called useRef and how we can use it to achieve infinite scrolling within our React.js application. So lets get started, shall we?

Short version of this blog

If you are someone who is just looking for the best method to use while implementing infinite scrolling and do not have a lot of time to read through a complete blog to understand the reason behind why we are choosing which method (which btw you totally should go through) here is the short answer to that :

  • Avoid trying to detect if the bottom of the page is reached or not.
  • Implement logic to detect if the last element from the dynamically rendered elements is in the viewport (visible area of your browser).
  • This has to be achieved using the useRef & useCallback hooks and storing reference to the last rendered element.
  • As soon as the last element is visible, re-fetch the next page of data (paginate).
  • Render the new data just bellow the existing elements.
  • Remove previous reference and re-assign reference to the last rendered element for the new data .
  • That should make the loop going on with infinite scrolling.
  • Check if there is any more data to fetch, if not, remove the reference to last element and do not assign it to anything and display prompt for no more data to load as you wish.

Github repo link - https://github.com/arunabharjun/articles-app

Okay so now that we have that out of the way, for those who got lost in the short version and are wondering what in the world am I talking about, don't worry, as we together will understand step by step along with code example what exactly is going on. But to understand that, we need to make sure we know what goes on behind the scenes when we render something in a React.js app and refresh ourselves with the underlying concepts of React.js .

So what is Virtual DOM in React.js

It is the in memory representation of the currently rendered elements in the React.js app and is synced with the “real” DOM using a library, like the ReactDOM.

A more detailed description can be found within React.js official documentation. As of writing this blog, the link to that is as follows -

Virtual DOM and Internals - React

Why do we need to understand this ?

Now the reason that I am bringing up the discussion abut the Virtual DOM in React.js is that I have seen that there is a tendency among us, the developers, while developing a React.js app, to forget about how the stuff is getting rendered as the states change and we simply get comfortable with the idea of letting create-react-app show us its magic and do what it does. And that might work for most small, less demanding projects, but for projects that require more than that, it is crucial that we understand what is going on under the hood when we are rendering UI elements in the React.js app.

Now having said that, and having understood what is Virtual DOM in React.js world, let us finally ask the question, what happens under the hood when we render UI elements in a React.js app? Let’s find out.

What happens under the hood in a React.js app

While we can go into in depth details on how re-rendering and infinite loops of renders can occur in a React.js app, but that is out of the scope for this particular blog. But in a nutshell, what happens under the hood is that React.js maintains a tree of UI components where every UI component have UI elements as their nodes and that UI element can in turn, again be another UI Component that has more UI elements inside that. So basically it can be visualised as a hierarchy of components within components and so on. But the key thing to note here is that we can visualise each UI element as a NODE of UI components tree. And that is what is going to help us understand why we are going to use the useRef hook to achieve infinite scrolling.

To understand about this in more details, React.js has an excellent blog in their official blog post page which you can give a read. The name of the blog post is React Components, Elements, and Instances and as of writing this blog, the link is as follows -

React Components, Elements, and Instances - React Blog

So now that we have brushed up on the basic concepts that we would require to understand the use of useRef hook in React.js, let us jump back to the goal of this blog, ie. implementing an infinitely scrolling SPA using React.js .

Possible logics that we can use to achieve infinite scrolling

  1. Detecting if the page has scrolled to the bottom of the page, and then loading new data and rendering it and that way achieving infinite scrolling.
  2. Checking if the last element rendered is in the view port (the visible area of your browser), and fetching new data when this condition is true and re checking if the last element is again visible in the viewport and this way the loop continues for infinite scrolling.

Problem with the 1st solution

Now while both the methods might seem like they would work, and in a lot of cases, they surely will, but there is a problem with the 1st solution. And that is it limits our implementation to only listening to the page scroll state, where we only load the data when we are at the very bottom of our page, and in many cases, it can trigger an infinite loop of the same request, eventually getting an error response from the server saying “429 : Too many requests”. And apart from that, you will encounter a number of many other issues too if you go the “detecting if page bottom was reached".

Why the 2nd solution is better ?

Now the second solution is much more flexible, and we can modify the logic to a lot of different iterations of it, like for example, we could also implement our own pull down to refresh page where we implement a UI element to not show by default and only show up when we pull down further, and as soon as the pull down element is in the view-port, we can refresh the data in our page. And that is just one example that I stated, but with this approach, you can think of more creative ways to trigger pagination/page-refresh/etc.

Also the second approach is the “React.js way” of doing it

Now that we have discussed why the second solution is in general a better approach, I believe it is time I can safely say that there is another good reason to make use of the second approach, and that is it allows us to achieve stuff the React.js way and not vanilla JS way, and to be honest, if we were to do everything the vanilla JS way, we would loose the significance behind using a UI library like React.js at the first place. And the React.js way to achieve infinite scrolling is by using the useRef hook to save reference to the last element that was rendered and do operations on that as we desire.

By now I have mentioned the useRef hook many times, but you might be asking, “Arunabh, what is this useRef hook that you keep talking about?” Let’s find out.

What is “ref”, “useRef” & "useCallback" in React.js ?

  1. Now, just like useState & useEffect, useRef is another hook which returns a mutable object. The returned object persists for the full lifetime of the component on which it is being used, unless instructed to do otherwise.
  2. Along with useRef, we are going to use another hook called useCallback that returns a memoized callback. It is something similar to useMemo, but for the purpose of achieving infinite-scrolling, we can safely use useCallback which you will understand how to in the later section of this blog.
  3. Now I hope you remember that we discussed about how React.js maintaines a tree of UI components and we can visualise each node to be a child component, and those nodes can be referred to, or in other words, we can pass reference to those nodes using the "ref" attribute in our component.

You can find out more about them from the official documentation of React.js. As of writing this blog, the links to that is as follows -

Hooks API Reference : useRef - React

Hooks API Reference : useCallback - React

Hooks API Reference : Documentation - React

Now these all might be appearing to be little confusing for now and you might be asking the question, "well how do we use them all together to achieve infinite scrolling ?". Lets find out.

Implementing infinite scroll

1 The first step will be to import the hooks, so lets get that out of the way

import { useRef, useCallback } from 'react';
Enter fullscreen mode Exit fullscreen mode

2 Now for the sake of simplicity, I will assume that you already know how to fetch data from an API and have data in your state already populated, and also are aware how to re populate data using pagination, so I will go straight to the part that deals with implementing infinite scroll.

3 So now, we will be setting up an observer which stores the object returned by useRef hook.

const observer = useRef();
Enter fullscreen mode Exit fullscreen mode

4 Now we will setup a function that stores the memoized callback function from useCallback hook to perform operation on the observer that was created in the last step.

const lastComponentRendered = useCallback(
        (node) => {
            if (loading) return;
            if (observer.current) observer.current.disconnect();
            observer.current = new IntersectionObserver((entries) => {
                if (entries[0].isIntersecting) {
                    fetchSomeData();
                }
            });
            if (node) observer.current.observe(node);
        },
        [
            loading
        ]
    );
Enter fullscreen mode Exit fullscreen mode

5 Let us breakdown the code in step 4. So we are returning the callback to the constant "lastComponentRendered" and passing a node (which you will understand how it works in the following steps).

const lastComponentRendered = useCallback(
        (node) => {
            //do stuff
        },
        []
    );
Enter fullscreen mode Exit fullscreen mode

6 Now to avoid infinite re-rendering, we need to keep a check if data pagination has already begun, and that is being stored in our "loading" state, which I will leave at you to implement as you wish.

const lastComponentRendered = useCallback(
        (node) => {
        if (loading) return;
        //do stuff
        },
      []
    );
Enter fullscreen mode Exit fullscreen mode

7 Now since in the 3rd step, we didn't pass any argument with the useRef hook, our observer will initially have a value of undefined and so we check if observer is undefined or not.

const lastComponentRendered = useCallback(
        (node) => {
            if (loading) return;
            if (observer.current) observer.current.disconnect();
            //do stuff
            },
          []
    );
Enter fullscreen mode Exit fullscreen mode

8 Now we reset the current property to be an instance of an intersection observer which basically holds an array of elements and returns true from the callback if the argument passed in the callback intersects with the view-port, in easy terms, lets us know if the UI Component is in view-port or not, when we check it with isIntersecting function. To know more about intersection observer, check out this https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API.

const lastComponentRendered = useCallback(
        (node) => {
            if (loading) return;
            if (observer.current) observer.current.disconnect();
            observer.current = new IntersectionObserver((entries) => {
                //do stuff
            });
            //do stuff
        },
        []
    );
Enter fullscreen mode Exit fullscreen mode

9 Now we simply check for the first element in the array of entries that we passed as argument in the callback function in IntersectionObserver() and se if it intersects.

const lastComponentRendered = useCallback(
        (node) => {
            if (loading) return;
            if (observer.current) observer.current.disconnect();
            observer.current = new IntersectionObserver((entries) => {
                if (entries[0].isIntersecting) {
                    //do stuff
                }
            });
            //do stuff
        },
        []
    );
Enter fullscreen mode Exit fullscreen mode

10 And if it does intersect, we simply paginate the data. I will leave it up to you to implement the pagination logic. Here that is being represented by the function "fetchSomeData()".

const lastComponentRendered = useCallback(
        (node) => {
            if (loading) return;
            if (observer.current) observer.current.disconnect();
            observer.current = new IntersectionObserver((entries) => {
                if (entries[0].isIntersecting) {
                    fetchSomeData();
                }
            });
            //do stuff
        },
        []
    );
Enter fullscreen mode Exit fullscreen mode

11 Now we simply observer's current property to observe the node that we passed as argument while calling the useCallback hook in the 4th step.

const lastComponentRendered = useCallback(
        (node) => {
            if (loading) return;
            if (observer.current) observer.current.disconnect();
            observer.current = new IntersectionObserver((entries) => {
                if (entries[0].isIntersecting) {
                    fetchSomeData();
                }
            });
            if (node) observer.current.observe(node);
            //stuff done
        },
        []
    );
Enter fullscreen mode Exit fullscreen mode

12 And just like the useEffect hook, we can pass a second argument as array of states on which the hook will depend and will only execute if there is a change in any of those states, and we pass the "loading" state for this purpose as we don't want it to execute for every re-render in the React.js app.

const lastComponentRendered = useCallback(
        (node) => {
            if (loading) return;
            if (observer.current) observer.current.disconnect();
            observer.current = new IntersectionObserver((entries) => {
                if (entries[0].isIntersecting) {
                    fetchSomeData();
                }
            });
            if (node) observer.current.observe(node);
            //stuff done
        },
        [
            loading
        ]
    );
Enter fullscreen mode Exit fullscreen mode

13 Now the only thing left to do is simply pass a reference of a UI Component (node) to "lastComponentRendered" using the "ref" attribute and see the magic happen.

return (
        <React.Fragment>
            <div className='container'>
                {fetchedData.map((data, i) => {
                    if (fetchedData.length === i + 1) {
                        return (
                            <div
                                ref={lastArticle}
                                key={i}
                            >
                                <YourCustomComponent>
                                    {data}           
                                </YourCustomComponent>
                            </div>
                        );
                    }
                    else
                        return (
                            <div key={i}>
                                <YourCustomComponent>
                                    {data}           
                                </YourCustomComponent>
                            </div>
                        );
                })}
            </div>
        </React.Fragment>
)
Enter fullscreen mode Exit fullscreen mode

14 And this step is very self explanatory, but for a better clarity, we are checking if the currently being rendered UI Component is the last one by checking if the length of "fetchedData" (which is the state that stores the data we fetch from our data source) is equal to the number of iterations that took place. And if that condition satisfies, we pass a reference for that UI Component using the "ref" attribute.

Full code implementation

I have implemented the logics that I explained in this blog in the following code. I encourage you to take a look at it to get an idea of the complete working of the concepts mentioned. The link is as follows -

arunabharjun/articles-app > Full-Code

You can also go ahead and clone the complete repo to get more in-depth understanding of the implementation. The repository README file has detailed explanation on how to get started with the project.

arunabharjun/articles-app

Bonus : Code snipped to detect bottom of page

Well, if you still wanted to see how to detect if the page had scrolled to the bottom or not, refer to the following code snippet.

/**
 * Utility function to listen for scrolling
 */
    const handleScroll = () => {
        const windowHeight =
            'innerHeight' in window
                ? window.innerHeight
                : document.documentElement.offsetHeight;
        const body = document.body;
        const html = document.documentElement;
        const docHeight = Math.max(
            body.scrollHeight,
            body.offsetHeight,
            html.clientHeight,
            html.scrollHeight,
            html.offsetHeight
        );
        const windowBottom = windowHeight + window.pageYOffset;
        if (windowBottom >= docHeight) {
            console.log("Bottom reached!");
        }
        else {
            console.log("Bottom not reached!");
        }
    };

Enter fullscreen mode Exit fullscreen mode

Conclusion

And so now you know how to make use of built-in features of React.js to implement a infinitely scrolling SPA with dynamic data fetching. Like this, there is a lot that React.js brings to the table and the more you explore, the more you'll know. So keep the hunger to explore alive and see you in the next blog.

Written By
-Arunabh Arjun
www.arunabharjun.com

Top comments (0)