Infinite-Scroll Image Cards in GatsbyJS

Divine Orji

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.

CodeSandbox & GitHub Repo

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.

Prerequisites

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

Setting up the Project

Open your terminal and run the command below in your preferred directory:

1gatsby 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.

Setting up your Images on Cloudinary

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.

Installing Dependencies for your Project

In your project’s terminal, run the command below to install the dependencies required for this project:

1npm 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 into CloudinaryMedia nodes in your Gatsby project

Configuring Plugins and Dependencies

In your code editor, open your gatsby-config.js file and update it with the code below:

1require('dotenv').config({
2 path: `.env.${process.env.NODE_ENV}`,
3});
4
5module.exports = {
6 siteMetadata: {
7 siteUrl: `https://www.yourdomain.tld`,
8 },
9 plugins: [
10 {
11 resolve: `gatsby-source-cloudinary`,
12 options: {
13 cloudName: process.env.CLOUDINARY_CLOUD_NAME,
14 apiKey: process.env.CLOUDINARY_API_KEY,
15 apiSecret: process.env.CLOUDINARY_API_SECRET,
16 resourceType: `image`,
17 prefix: `gatsby-infinite-scroll-images/`,
18 maxResults: 2000,
19 },
20 },
21 ],
22};

In the code above, you did the following:

  • Imported dotenv and configured it
  • Exported siteMetadata and your installed plugins with their configurations
  • siteMetadata contains metadata for your Gatsby site, add your site’s title, siteUrl, and description
  • In gatsby-source-cloudinary, the cloudName, apiKey, and apiSecret will contain your Cloudinary credentials gotten from a .env file
  • options: resourceType, prefix, and maxResults 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 the gatsby-infinite-scroll-images/ folder created on Cloudinary

Storing your Cloudinary Credentials in a .env file

In your project’s root folder, create a .env.development file and fill it with the data below:

1CLOUDINARY_CLOUD_NAME=<Your Cloudinary Cloud Name here>
2CLOUDINARY_API_KEY=<Your Cloudinary API Key here>
3CLOUDINARY_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:

1node_modules/
2.cache/
3public
4*env.*

Getting Cloudinary Media through Gatsby’s GraphQL Layer

In your code editor, update src/pages/index.js with the code below:

1import { graphql, useStaticQuery } from 'gatsby';
2import React from 'react';
3
4export default function Home() {
5 // === CLOUDINARY MEDIA ===
6 // Get images from Cloudinary with Gatsby's useStaticQuery hook
7 const data = useStaticQuery(graphql`
8 query CloudinaryImages {
9 allCloudinaryMedia {
10 edges {
11 node {
12 public_id
13 secure_url
14 }
15 }
16 }
17 }
18 `);
19 const cldImages = data.allCloudinaryMedia.edges;
20 console.log(cldImages);
21
22 return (
23 <div style={{ width: '100%', maxWidth: '425px', margin: '0 auto' }}>
24 <h1>Home</h1>
25 </div>
26 );
27}

In the code above, you did the following:

  • Queried CloudinaryImages in Gatsby’s GraphQL layer using useStaticQuery to access an array of nodes in allCloudinaryMedia
  • Got the public_id and secure_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:

1npm run develop

Once the server is up and running, open localhost:8000 on your browser and check the console:

Creating an Image Component

In your project’s src/ folder, create an Image.js file inside a components/ folder and fill it with the code below:

1import React from ‘react’;
2
3export default function Image({ image }) {
4 return (
5 <div style={{ width: '100%', maxWidth: '425px', height: '425px' }}>
6 <img
7 src={image.node.secure_url}
8 alt={image.node.public_url}
9 style={{ objectFit: 'cover', width: '100%', height: '100%' }}
10 />
11 </div>
12 );
13}

In the code above, you did the following:

  • Created an Image component with an image prop
  • Used the image prop to add your secure_url to src and public_url to alt

In your src/pages/index.js file, import your newly created Image component and update the return statement with the code below:

1return (
2 <div style={{ width: '100%', maxWidth: '425px', margin: '0 auto' }}>
3 <h1>Home</h1>
4
5 <section style={{ display: 'grid', gap: '2rem' }}>
6 {cldImages &&
7 cldImages.map((imageNode, index) => (
8 <Image key={`${index}-cld`} image={imageNode} />
9 ))}
10 </section>
11 </div>
12);

In the code above, you did the following:

  • Created a <section> element and mapped the contents of your cldImages array to create an Image component for each element in the array
  • Added an imageNode parameter to the image attribute of each Image 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:

Setting a Limit to the Number of Images Displayed while Scrolling

In your code editor, update src/pages/index.js with the code below:

1import { graphql, useStaticQuery } from 'gatsby';
2import React, { useEffect, useState } from 'react';
3import Image from '../components/Image';
4
5export default function Home() {
6 // === CLOUDINARY MEDIA ===
7 // Get images from Cloudinary with Gatsby's useStaticQuery hook
8 // (Do not modify the pre-existing code here, just add the new ones below.)
9
10 // === INFINITE SCROLL LOGIC ===
11 // Incrementally increase the number of images to display while scrolling
12 const [imagesList, setImagesList] = useState([]);
13 const [limit, setLimit] = useState(4);
14 const start = imagesList.length;
15
16 function newLimit() {
17 const blip = cldImages.length - start;
18 if (blip > 5) {
19 setLimit(limit + 5);
20 } else {
21 setLimit(limit + blip);
22 }
23 }
24
25 useEffect(() => {
26 const temp = [];
27 for (let i = limit; i >= start; i--) {
28 temp.push(cldImages[i]);
29 }
30 setImagesList((prev) => [...prev, ...temp]);
31 }, [cldImages, limit, start]);
32
33return (
34 <div style={{ width: '100%', maxWidth: '425px', margin: '0 auto' }}>
35 <h1>Home</h1>
36 <section style={{ display: 'grid', gap: '2rem' }}>
37 {imagesList &&
38 imagesList.map((imageNode, index) => (
39 <Image key={`${index}-cld`} image={imageNode} />
40 ))}
41 </section>
42 </div>
43 );
44}

In the code above, you did the following:

  • Created an imagesList with useState([]) and a limit variable with useState(4)
  • Implemented a useEffect() that loops through cldImages and pushes each element into a temp array, then adds the array to your existing imagesList array after the loop is complete
  • Mapped and displayed each element of imagesList as an Image component
  • Created a newLimit() function that updates the limit variable when triggered

Checking When the Last Image in the Array is in the Viewport using Intersection Observer

Update src/components/Image.js with the code below:

1import React, { useEffect, useRef } from 'react';
2
3export default function Image({ image, isLast, newLimit }) {
4 const imageRef = useRef();
5
6 useEffect(() => {
7 if (!imageRef?.current) return;
8 const observer = new IntersectionObserver(([entry]) => {
9 if (isLast && entry.isIntersecting) {
10 newLimit();
11 observer.unobserve(entry.target);
12 }
13 });
14 observer.observe(imageRef.current);
15 }, [isLast]);
16
17 return (
18 <div style={{ width: '100%', maxWidth: '425px', height: '425px' }}>
19 <img
20 ref={imageRef}
21 src={image.node.secure_url}
22 alt={image.node.public_url}
23 style={{ objectFit: 'cover', width: '100%', height: '100%' }}
24 />
25 </div>
26 );
27}

In the code above, you did the following:

  • Added two new props - isLast checks if the last image in your imagesList array is in the viewport and newLimit triggers the newLimit() function in src/pages/index.js
  • Created an imageRef variable to access your <img> tag with useRef
  • Created an observer variable that is an instance of IntersectionObserver, if isLast and entry.isIntersecting is true, it triggers the newLimit() 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 its current value is null, the useEffect hook ends without running the observer

In your src/pages/index.js, update the <section> tag in your return statement with the code below:

1<section style={{ display: 'grid', gap: '2rem' }}>
2 {imagesList &&
3 imagesList.map((imageNode, index) => (
4 <Image
5 key={`${index}-cld`}
6 image={imageNode}
7 isLast={index === imagesList.length - 1}
8 newLimit={newLimit}
9 />
10 ))}
11</section>

In the code above, you did the following:

  • Set your isLast prop to check if the current index equals the last element in the imagesList array
  • Set your newLimit() function as a value in your newLimit prop

In your browser, refresh localhost:8000 and scroll down to see the infinite scroll in action:

Conclusion

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:

Resources

Divine Orji

Software Engineer and Technical Writer

I am a software engineer passionate about building fast, scalable apps with beautiful user interfaces.