How to Create an Infinite Scroll Component in NextJS / React (Updated)
Tags: nextjs, react
Published: 2023-02-19 02:48:01

There are many times you'll want to use a NextJS or React library to accomplish more complex coding tasks, and there are times when you actually should *reinvent the wheel*, as it were. And creating an infinite scroll component is one of those times because it doesn't require a whole lot of code and it's always a good idea to reduce the number of dependencies your project relies on.

In this demo, I will be using a NextJS v. 13 project spun up using `create-next-app@latest`. If you want, you can simply clone [this repo](https://github.com/designly1/nextjs-infinite-scroll-example). Also, you can check out the [live demo](https://nextjs-infinite-scroll-example.vercel.app/).

I've used the following packages for this example:

| Package Name | Description |
| -------------- | ------------ |
| axios | Promise based HTTP client for the browser and node.js |
| react-uuid | A simple library to create uuid's in React |

NOTE: I've updated this tutorial because I came up a better solution that I liked so much more than the old one that I had to change it!

Here's our product component, very basic:

```js
// Product.js
import React from 'react'

export default function Product({ product }) {
    if (product) {
        const curr = new Intl.NumberFormat('en-US', {
            style: 'currency',
            currency: 'USD'
        });
        return (
            <div className="product">
                <h2>{product.title}</h2>
                <h3>{curr.format(product.price)}</h3>
                <img src={product.image} alt={product.title} />
            </div>
        )
    }
}
```

Note that I am not using `next/image` to render the product image because the fake API images do not all have the same dimensions (super annoying), and doing fluid images with `next/image` is a pain. If you would like more information on how to do this, see [this article](https://designly.biz/blog/post/creating-a-modern-hero-image-using-nextjs-13-images-and-tailwind-css).

<ad></ad>

Here's our custom hook that we can use in any component:

```js
// useInfiniteScroll.js
import React, { useLayoutEffect } from 'react'

export default function useInfiniteScroll({
    trackElement, // Element placed at bottom of scroll container
    containerElement, // Scroll container, window used if not provided
    multiplier = 1 // Adjustment for padding, margins, etc.
}, callback) {
    useLayoutEffect(() => {
        // Element whose position we want to track
        const ele = document.querySelector(trackElement);

        // If not containerElement provided, we use window
        let container = window;
        if (containerElement) {
            container = document.querySelector(containerElement);
        }

        // Get window innerHeight or height of container (if provided)
        let h;
        if (containerElement) {
            h = container.getBoundingClientRect().height;
        } else {
            h = container.innerHeight;
        }

        const handleScroll = () => {
            const elePos = ele.getBoundingClientRect().y;
            if (elePos * multiplier <= h) {
                if (typeof callback === 'function') callback();
            }
        }

        // Set a passive scroll listener on our container
        container.addEventListener('scroll', handleScroll, { passive: true });

        // handle cleanup by removing scroll listener
        return () => container.removeEventListener('scroll', handleScroll, { passive: true });
    })
}
```

And here's our `index.js` page:

```js
// index.js
import React, { useState, useEffect } from 'react'
import Head from 'next/head'
import { Inter } from '@next/font/google'
import axios from 'axios'
import uuid from 'react-uuid'
import Product from '@/components/Product'
import useInfiniteScroll from '@/hooks/useInfiniteScroll'

const inter = Inter({ subsets: ['latin'] })

export default function Home({ products }) {
  // Create state to store number of products to display
  const [displayProducts, setDisplayProducts] = useState(3);

  // Invoke our custom hook
  useInfiniteScroll({
    trackElement: '#products-bottom',
    containerElement: '#main'
  }, () => {
    setDisplayProducts(oldVal => oldVal + 3);
  });

  return (
    <>
      <Head>
        <title>NextJS Infinite Scroll Example</title>
        <meta name="description" content="NextJS infinite scroll example by Designly." />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main id="main" className={inter.className}>
        <div className="container">
          <h1 style={{ textAlign: 'center' }}>Products Catalog</h1>
          {
            products.slice(0, displayProducts).map((product) => (
              <Product key={uuid()} product={product} />
            ))
          }
          <div id="products-bottom"></div>
        </div>
      </main>
    </>
  )
}

// Get our props from the remote API via ISR
export async function getStaticProps() {
  let products = [];

  try {
    const res = await axios.get('https://fakestoreapi.com/products');
    products = res.data;
  } catch (e) {
    console.error(e);
  }

  return {
    props: {
      products
    },
    revalidate: 10
  }
}
```

Here's the breakdown of this code:

1. We create a state to hold the number of products we want to display.
2. We invoke our custom `useInfiniteScroll()` hook to track the position of our invisible element at the bottom of the products list.
3. We use `getStaticProps()` to statically generate our product data, but you could use `getServersideProps` or use the `axios` request client-side as well. Normally, you would want to use `getStaticProps` on  a NextJS app because, well, that's the whole point for using NextJS!

And last but not least, the CSS:

```css
/* globals.css */
html,
body {
  margin: 0;
  padding: 0;
  overflow: hidden;
}

body {
  position: relative;
}

main {
  background-color: rgb(54, 77, 107);
  color: white;
  width: 100vw;
  height: 100vh;
  margin: 0;
  position: fixed;
  top: 0;
  bottom: 0;
  right: 0;
  left: 0;
  overflow-x: hidden;
  overflow-x: auto;
}

.container {
  max-width: 1200px;
  margin: auto;
  width: fit-content;
  padding: 10px;
}

.product {
  max-width: 600px;
  background-color: rgb(32, 43, 56);
  color: white;
  padding: 1em;
  margin-bottom: 2em;
  display: flex;
  flex-direction: column;
}

.product > * {
  margin-left: auto;
  margin-right: auto;
}

.product img {
  width: 99%;
  height: auto;
}

.product h3 {
  color: rgb(57, 181, 253);
}
```

The key to the function of our infinite scroll component is the CSS of the `main` element. We set this to be a fixed position covering the whole viewport and then set the scroll to `auto`. This allows our main container to do the scrolling rather than `body`.

First, we set `html, body` to `overflow:hidden` to prevent horizontal and vertical scroll bars from appearing. This is especially important for mobile devices. There's nothing more unprofessional than a mobile page with horizontal scroll bars! Then we set our `main` to `overflow-x:hidden` and `overflow-y:auto` to show only a vertical scroll bar when its content exceeds the viewport height. Lastly, we use static positioning on the `main` element to ensure that it covers the entire viewport.

Thank you for taking the time to read my article and I hope you found it useful (or at the very least, mildly entertaining). For more great information about web dev, systems administration and cloud computing, please read the [Designly Blog](https://designly.biz/blog). Also, please leave your comments! I love to hear thoughts from my readers.

I use [Hostinger](https://hostinger.com?REFERRALCODE=1J11864) to host my clients' websites. You can get a business account that can host 100 websites at a price of $3.99/mo, which you can lock in for up to 48 months! It's the best deal in town. Services include PHP hosting (with extensions), MySQL, Wordpress and Email services.

Looking for a web developer? I'm available for hire! To inquire, please fill out a [contact form](https://designly.biz/contact).