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>
)
}
}
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
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:
- We create a state to hold the number of products we want to display.
- We invoke our custom
useInfiniteScroll()
hook to track the position of our invisible element at the bottom of the products list. - We use
getStaticProps()
to statically generate our product data, but you could usegetServersideProps
or use theaxios
request client-side as well. Normally, you would want to usegetStaticProps
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);
}
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.