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 [email protected]. 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">
                <img src={product.image} alt={product.title} />

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 });

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
    trackElement: '#products-bottom',
    containerElement: '#main'
  }, () => {
    setDisplayProducts(oldVal => oldVal + 3);
  return (
        <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" />
      <main id="main" className={inter.className}>
        <div className="container">
          <h1 style=>Products Catalog</h1>
            products.slice(0, displayProducts).map((product) => (
              <Product key={uuid()} product={product} />
          <div id="products-bottom"></div>
// Get our props from the remote API via ISR
export async function getStaticProps() {
  let products = [];
  try {
    const res = await axios.get('');
    products =;
  } catch (e) {
  return {
    props: {
    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:

/* globals.css */
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.

I truly hope you found this article helpful! For more great tutorials on web dev and systems administration, please read the Designly Blog.