DEV Community

Cover image for How to Create an Infinite Scroll Component in NextJS / React (Updated)
Jay @ Designly
Jay @ Designly

Posted on • Edited on • Originally published at blog.designly.biz

How to Create an Infinite Scroll Component in NextJS / React (Updated)

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. Also, you can check out the live demo.

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:

// 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>
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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

// 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 });
    })
}
Enter fullscreen mode Exit fullscreen mode

And here's our index.js page:

// 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
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

/* 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);
}
Enter fullscreen mode Exit fullscreen mode

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. Also, please leave your comments! I love to hear thoughts from my readers.

I use Hostinger 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.

Top comments (0)