Implementing infinite scrolling in React
Infinite scrolling is a web-design technique that loads content continuously as the user scrolls down the page. The infinite scrolling mechanism automatically retrieves data from an API when needed without requiring the user to perform an extra action.
In this article, we will learn how to implement the infinite scrolling technique in React apps. We will use a React hook to fetch random words from a REST API that provides a paginated API endpoint; then, we will build a component to render the list of random words retrieved from the REST API. We will also learn about the Intersection Observer API and how it can be used to determine when to retrieve more content.
Project Setup
To set up the project, run the following on your terminal:
npx create-react-app infinite-scrolling
cd infinite-scrolling
npm install axios
npm start
Let’s create a React hook that handles pagination and parses data from the REST API to get started. We’ll be using Axios for HTTP requests.
The REST API endpoint requires the following query parameters:
page
: Indicates what page/offset to return data from.limit
: Indicates the number of items to be returned per page.
For example, https://randommockly.herokuapp.com/random/words?page=1&limit=10
returns the following:
{
"success": true,
"message": "Random words",
"data": [
"height",
"goal",
"beat",
"create",
"big",
"margin",
"Israeli",
"develop",
"cut",
"it"
],
"pageData": {
"total": 100,
"currentPage": 1,
"nextPage": 2,
"prevPage": null,
"lastPage": 10
}
}
The pageData
attribute contains some vital page information like:
nextPage
: Specifies the value of the next page,prevPage
: Specifies the value of the previous page.total
: The total number of data items.
Now create a folder name hooks
inside the src
folder. Within this folder, create a file named usePagination.js
and add the following code:
import axios from "axios";
import { useEffect, useState } from "react";
export default function usePagination(page) {
const [data, setData] = useState([]); // stores data retrieved
const [more, setMore] = useState(true); // indicates if there's more data to be retrieved from the API.
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false); // indicates error existence
useEffect(() => {
// Reset error state before loading
setError(false);
// indicate that data-fetching is ongoing
setLoading(true);
// make call to API
axios.get('https://randommockly.herokuapp.com/random/words', {
params: { // query params
page,
limit: 10,
}
}).then(({ data: resData }) => {
// add fetched data to list. We make use of Set to avoid duplicates
setData((prev) => [...new Set([...prev, ...resData.data])]);
setMore(Boolean(resData.pageData.nextPage)); // set more to true if nextPage exists
setLoading(false);
})
.catch(() => setError(true)); // set error to true if an exception was caught
}, [page]);
return { data, more, loading, error };
}
The hook above fetches more content from the REST API when the value of page
increases. This is done with the help of the useEffect hook
that triggers the API call every time the value of page
changes.
The hook returns useful data that the component will use. These include:
data
: List of data fetched.more
: Indicates if there are more resources to be fetched.loading
: Indicates when data-fetching is ongoing.error
: Indicates if an error has occurred while fetching resources
Let’s now create our list component that uses the usePagination
hook to fetch and display data (list of random words).
Edit the App.js
file to contain the following:
import { useState, useRef, useCallback } from 'react';
import './App.css';
import usePagination from './hooks/usePagination';
function App() {
const [page, setPage] = useState(1);
// get data returned from hooks
const { data, more, loading, error } = usePagination(page);
// Observer
return (
<div className="App">
<ul className="list-container">
{/** Render list of words from data*/}
{data.map((word, index) => (
<li
className="list-item"
key={`${word}-${index}`}
// ref
>
{word}
</li>
))}
{/** if data-fetching is in progress, the indicator below is rendered */}
{loading && (
<li id="loader">Loading...</li>
)}
</ul>
{/** if there's no more data to be fetched, render an indicator to inform the user */}
{!more && <div id="end">You've the reached the end</div>}
</div>
);
}
export default App;
We added the usePagination
hook and a page state in the list component. Whenever the value of page
changes, the usePagination
hook will be fired, and data for the new page will be fetched from the API.
Two elements are also rendered conditionally. The loader
element gets rendered when loading
is true
to inform the user that more data is being fetched. The div
element with an id of end
is rendered when there is no more data to be fetched from the REST API (i.e., when more
is false
).
Also, edit App.css
and add the following CSS rules:
.list-container {
padding: 0;
margin: 0;
list-style: none;
}
.list-item {
padding: 12px;
cursor: pointer;
}
.list-item:hover {
background: rgba(0, 0, 0, .05);
}
#loader {
color: rgba(0, 0, 235, 0.5);
padding: 12px;
cursor: progress;
}
#end {
padding: 12px;
font-weight: 700;
}
If you head on to your browser, you’ll find that our app displays a list of 10 words. This is the first page of data from the REST API (10 data items are returned because we specified a limit of 10 in the query):
How does the component retrieve more content from the API when the user scrolls to the last item in the list? This is where Intersection Observer API comes in. Let’s dig a little into it.
Open Source Session Replay
OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.
Start enjoying your debugging experience - start using OpenReplay for free.
What is the Intersection Observer API?
The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport. It can be used to determine the visibility of an element in relation to another element or the device’s viewport. This means that it can be used to detect when an element becomes visible on the user’s screen/viewport or within an element.
How then can the Intersection Observer API be used to implement infinite scrolling?
To implement infinite scrolling:
- The Intersection Observer API can determine when the last element in the list becomes visible in the viewport or the parent element.
- At this point, we can increment the value of
page
; this will trigger the usePagination hook to fetch more items from the source. - This cycle is repeated until the last set of data is retrieved from the source (in this case, the REST API).
Let’s get into creating the observer. We’ll use a React callback ref and IntersectionObserver API to keep track of the last element in the list. Edit the App.js file, and add the following code to the App component. Just under the // Observer
comment:
// Observer
const observer = useRef(); // ref to store observer
const lastElementRef = useCallback((element) => {
//element is the react element being referenced
// disconnect observer set on previous last element
if (observer.current) observer.current.disconnect();
// if there's no more data to be fetched, don't set new observer
if (!more) return;
// set new observer
observer.current = new IntersectionObserver((entries) => {
// increase page number when element enters (is intersecting with) viewport.
// This triggers the pagination hook to fetch more items in the new page
if (entries[0].isIntersecting && more) setPage((prev) => prev + 1);
});
// observe/monitor last element
if (element) observer.current.observe(element);
}, [more]);
Here’s a break-down of the code above:
- A ref is used to store the observer because we want the observer variable to be mutable. We also use the
useCallback
hook withmore
as a dependency to prevent re-creation of the function on every re-render. - Inside the callback, we disconnect the observer monitoring the previous last element and set a new observer to watch the current last element. Note that the function parameter
element
refers to the element referenced using theref
prop. - Inside the IntersectionObserver callback, a check is made to see if the first entry (the element being monitored) intersects with the viewport (visible in the viewport) and if there are more items to be fetched. If these conditions are true, the value of
page
is incremented. This triggers theusePagination
hook to fetch more items for the new page.
Finally, let’s add the lastElementRef
to the last element in the list.
Edit the App component and add the conditional ref to the list component so that it looks like this:
<li
className="list-item"
key={`${word}-${index}`}
// ref
ref={index === data.length - 1 ? lastElementRef : undefined}
>
{word}
</li>
From the code above, the lastElementRef
is set on the last element in the list by checking if the index equals that of the last item in the array.
When we run the app on the browser, we get the following result:
Conclusion
In this article, you learned how to implement infinite scrolling in React apps. We looked at the Intersection Observer API and how it can be used to implement infinite scrolling.
A TIP FROM THE EDITOR: React Query also provides an alternative: check out our Infinite scrolling with React Query article. And, if you are curious about solutions with other frameworks, don’t miss the Infinite Scrolling in Angular and Infinite Scrolling in Vue using the Vue Intersection Observer API articles.