Infinite scrolling is a web design technique where the entire content of a website or app does not load on the page at once but loads incrementally as a user scrolls downward, giving the website a sense of being infinite. This technique creates a seamless user experience and keeps users active.
In this article, you will learn how to implement infinite-scroll in a Gatsby.js website using JavaScript’s Intersection Observer API.
The complete demo for this article is on CodeSandbox. Fork and run it to quickly get started.
To view its source code on GitHub, click this GitHub link.
Understanding this article requires the following:
- Knowledge of JavaScript and React, especially React Hooks
- A Cloudinary account for storing and delivering your images (create a free account here)
- Installation of Node.js on your local machine
- Installation of Gatsby CLI on your local machine, knowledge of Gatsby.js is preferable but not strictly required
Open your terminal and run the command below in your preferred directory:
gatsby new
Set the recommended options below in the series of prompts from Gatsby’s CLI, and hit “Enter” on your keyboard to proceed with the project setup:
After a successful project setup, view your project’s codebase in your preferred code editor.
In your browser, open your Cloudinary dashboard and click on the “Media Library” tab:
Upload your chosen images for this project in a new folder:
You can get some images from Lorem Picsum.
In your project’s terminal, run the command below to install the dependencies required for this project:
npm i dotenv gatsby-source-cloudinary
-
dotenv
gives you access to any data in a.env
file -
gatsby-source-cloudinary
queries media files from your Cloudinary account intoCloudinaryMedia
nodes in your Gatsby project
In your code editor, open your gatsby-config.js
file and update it with the code below:
require('dotenv').config({
path: `.env.${process.env.NODE_ENV}`,
});
module.exports = {
siteMetadata: {
siteUrl: `https://www.yourdomain.tld`,
},
plugins: [
{
resolve: `gatsby-source-cloudinary`,
options: {
cloudName: process.env.CLOUDINARY_CLOUD_NAME,
apiKey: process.env.CLOUDINARY_API_KEY,
apiSecret: process.env.CLOUDINARY_API_SECRET,
resourceType: `image`,
prefix: `gatsby-infinite-scroll-images/`,
maxResults: 2000,
},
},
],
};
In the code above, you did the following:
- Imported
dotenv
and configured it - Exported
siteMetadata
and your installedplugins
with their configurations -
siteMetadata
contains metadata for your Gatsby site, add your site’stitle
,siteUrl
, anddescription
- In
gatsby-source-cloudinary
, thecloudName
,apiKey
, andapiSecret
will contain your Cloudinary credentials gotten from a.env
file -
options
:resourceType
,prefix
, andmaxResults
will tell the plugin what kind of media file you want, where to get it, and how many should be returned, you get a maximum of 2000 images from thegatsby-infinite-scroll-images/
folder created on Cloudinary
In your project’s root folder, create a .env.development
file and fill it with the data below:
CLOUDINARY_CLOUD_NAME=<Your Cloudinary Cloud Name here>
CLOUDINARY_API_KEY=<Your Cloudinary API Key here>
CLOUDINARY_API_SECRET=<Your Cloudinary API Secret here>
Navigate to your Cloudinary Dashboard in your browser to get your Cloud Name, API Key, and API Secret:
In your code editor, update the .gitignore
file with the code below to prevent your .env
files from storing on Git:
node_modules/
.cache/
public
*env.*
In your code editor, update src/pages/index.js
with the code below:
import { graphql, useStaticQuery } from 'gatsby';
import React from 'react';
export default function Home() {
// === CLOUDINARY MEDIA ===
// Get images from Cloudinary with Gatsby's useStaticQuery hook
const data = useStaticQuery(graphql`
query CloudinaryImages {
allCloudinaryMedia {
edges {
node {
public_id
secure_url
}
}
}
}
`);
const cldImages = data.allCloudinaryMedia.edges;
console.log(cldImages);
return (
<div style={{ width: '100%', maxWidth: '425px', margin: '0 auto' }}>
<h1>Home</h1>
</div>
);
}
In the code above, you did the following:
- Queried
CloudinaryImages
in Gatsby’s GraphQL layer usinguseStaticQuery
to access an array of nodes inallCloudinaryMedia
- Got the
public_id
andsecure_url
of each Cloudinary media file present in its node - Stored the array of nodes in a
cldImages
variable and displayed it in your browser’s console
In your terminal, start the development server with the command below:
npm run develop
Once the server is up and running, open localhost:8000
on your browser and check the console:
In your project’s src/
folder, create an Image.js
file inside a components/
folder and fill it with the code below:
import React from ‘react’;
export default function Image({ image }) {
return (
<div style={{ width: '100%', maxWidth: '425px', height: '425px' }}>
<img
src={image.node.secure_url}
alt={image.node.public_url}
style={{ objectFit: 'cover', width: '100%', height: '100%' }}
/>
</div>
);
}
In the code above, you did the following:
- Created an
Image
component with animage
prop - Used the
image
prop to add yoursecure_url
tosrc
andpublic_url
toalt
In your src/pages/index.js
file, import your newly created Image
component and update the return
statement with the code below:
return (
<div style={{ width: '100%', maxWidth: '425px', margin: '0 auto' }}>
<h1>Home</h1>
<section style={{ display: 'grid', gap: '2rem' }}>
{cldImages &&
cldImages.map((imageNode, index) => (
<Image key={`${index}-cld`} image={imageNode} />
))}
</section>
</div>
);
In the code above, you did the following:
- Created a
<section>
element and mapped the contents of yourcldImages
array to create anImage
component for each element in the array - Added an
imageNode
parameter to theimage
attribute of eachImage
component, which enables you to access{node: {public_id: <value>, secure_url: <value>}}
from the array
When you refresh localhost:8000
on your browser, you should see something similar to this below:
In your code editor, update src/pages/index.js
with the code below:
import { graphql, useStaticQuery } from 'gatsby';
import React, { useEffect, useState } from 'react';
import Image from '../components/Image';
export default function Home() {
// === CLOUDINARY MEDIA ===
// Get images from Cloudinary with Gatsby's useStaticQuery hook
// (Do not modify the pre-existing code here, just add the new ones below.)
// === INFINITE SCROLL LOGIC ===
// Incrementally increase the number of images to display while scrolling
const [imagesList, setImagesList] = useState([]);
const [limit, setLimit] = useState(4);
const start = imagesList.length;
function newLimit() {
const blip = cldImages.length - start;
if (blip > 5) {
setLimit(limit + 5);
} else {
setLimit(limit + blip);
}
}
useEffect(() => {
const temp = [];
for (let i = limit; i >= start; i--) {
temp.push(cldImages[i]);
}
setImagesList((prev) => [...prev, ...temp]);
}, [cldImages, limit, start]);
return (
<div style={{ width: '100%', maxWidth: '425px', margin: '0 auto' }}>
<h1>Home</h1>
<section style={{ display: 'grid', gap: '2rem' }}>
{imagesList &&
imagesList.map((imageNode, index) => (
<Image key={`${index}-cld`} image={imageNode} />
))}
</section>
</div>
);
}
In the code above, you did the following:
- Created an
imagesList
withuseState([])
and alimit
variable withuseState(4)
- Implemented a
useEffect()
that loops throughcldImages
and pushes each element into atemp
array, then adds the array to your existingimagesList
array after the loop is complete - Mapped and displayed each element of
imagesList
as anImage
component - Created a
newLimit()
function that updates thelimit
variable when triggered
Update src/components/Image.js
with the code below:
import React, { useEffect, useRef } from 'react';
export default function Image({ image, isLast, newLimit }) {
const imageRef = useRef();
useEffect(() => {
if (!imageRef?.current) return;
const observer = new IntersectionObserver(([entry]) => {
if (isLast && entry.isIntersecting) {
newLimit();
observer.unobserve(entry.target);
}
});
observer.observe(imageRef.current);
}, [isLast]);
return (
<div style={{ width: '100%', maxWidth: '425px', height: '425px' }}>
<img
ref={imageRef}
src={image.node.secure_url}
alt={image.node.public_url}
style={{ objectFit: 'cover', width: '100%', height: '100%' }}
/>
</div>
);
}
In the code above, you did the following:
- Added two new props –
isLast
checks if the last image in yourimagesList
array is in the viewport andnewLimit
triggers thenewLimit()
function insrc/pages/index.js
- Created an
imageRef
variable to access your<img>
tag withuseRef
- Created an
observer
variable that is an instance ofIntersectionObserver
, ifisLast
andentry.isIntersecting
is true, it triggers thenewLimit()
function - After the
newLimit()
is triggered,observer.unobserve(entry.target)
ensures that Intersection Observer does not track the target image any further - Initialized the Intersection Observer using
observer.observe(imageRef.current)
- If
imageRef
does not exist or itscurrent
value isnull
, theuseEffect
hook ends without running theobserver
In your src/pages/index.js
, update the <section>
tag in your return
statement with the code below:
<section style={{ display: 'grid', gap: '2rem' }}>
{imagesList &&
imagesList.map((imageNode, index) => (
<Image
key={`${index}-cld`}
image={imageNode}
isLast={index === imagesList.length - 1}
newLimit={newLimit}
/>
))}
</section>
In the code above, you did the following:
- Set your
isLast
prop to check if the currentindex
equals the last element in theimagesList
array - Set your
newLimit()
function as a value in yournewLimit
prop
In your browser, refresh localhost:8000
and scroll down to see the infinite scroll in action:
In this article, you learned how to implement infinite scroll in your Gatsby.js website using Intersection Observer. To explore the full capabilities of Gatsby.js in building fast, performant web applications, check out the resources below: