Hello folks,
Few days back I came across the use-case of infinite scroll in React. For this, I used Intersection Observer and found different ways of implementing it in infinite scrolling.
Before we dive in, let's first understand our problem statement better. Consider an API which gives you list of users and some of their basic details. The task here is to show list of all users in cards. Simple right?
Now, consider there are thousands of users and the API we are using is paginated. In this case, there will be these two ways to use our paginated API -
- Use next/prev buttons to go through different pages
- Use infinite scroll
As the article title says, we are going with 2nd approach.ð
Now, let's see how?
- We will be calling our API to get first 25 results.
- Once the user scrolls through the list and reach to the last element, we will make another API call and pull next set of users in the view.
This way, even if user keep scrolling, they will always see list of users until they reach till the end.
Before moving to the implementation part, let me give you the brief idea of Intersection Observer
What is Intersection Observer?
The Intersection Observer is a browser API that provides a way to asynchronously observe or detect visibility of two elements in relation to each other.
As per MDN, this API is mostly used for performing visibility related tasks which includes lazy-loading of images and implementing "infinite scrolling" web sites, where more and more content is loaded and rendered as you scroll.
You can check detailed information of Intersection Observer here.
Implementing Infinite Scroll
For the infinite scrolling we will be using an open source RandomUserAPI.
For basic project setup, I created a simple React project with create-react-app and added Tailwind CSS to it. Also, for calling APIs, I added axios to the same project.
I have divided the implementation in 2 steps as follows -
1. Calling API, storing and displaying data.
With our basic setup in place, let's see the first version of code where we are calling a user API to get the list of users.
// app.js
import axios from 'axios';
import { useEffect, useState } from 'react';
const TOTAL_PAGES = 3;
const App = () => {
const [loading, setLoading] = useState(true);
const [allUsers, setAllUsers] = useState([]);
const [pageNum, setPageNum] = useState(1);
const callUser = async () => {
setLoading(true);
let response = await axios.get(
`https://randomuser.me/api/?page=${pageNum}&results=25&seed=abc`
);
setAllUsers(response.data.results);
setLoading(false);
};
useEffect(() => {
if (pageNum <= TOTAL_PAGES) {
callUser();
}
}, [pageNum]);
const UserCard = ({ data }) => {
return (
<div className='p-4 border border-gray-500 rounded bg-white flex items-center'>
<div>
<img
src={data.picture.medium}
className='w-16 h-16 rounded-full border-2 border-green-600'
alt='user'
/>
</div>
<div className='ml-3'>
<p className='text-base font-bold'>
{data.name.first} {data.name.last}
</p>
<p className='text-sm text-gray-800'>
{data.location.city}, {data.location.country}
</p>
<p className='text-sm text-gray-500 break-all'>
{data.email}
</p>
</div>
</div>
);
};
return (
<div className='mx-44 bg-gray-100 p-6'>
<h1 className='text-3xl text-center mt-4 mb-10'>All users</h1>
<div className='grid grid-cols-3 gap-4'>
{allUsers.length > 0 &&
allUsers.map((user, i) => {
return (
<div key={`${user.name.first}-${i}`}>
<UserCard data={user} />
</div>
);
})}
</div>
{loading && <p className='text-center'>loading...</p>}
</div>
);
};
export default App;
This is how our page will look like ð
The code is pretty straightforward. In the callUser
function, we are calling the API and storing the result in allUsers
state. Below, we are showing each user from the allUsers
array using a card component UserCard
.
You will see one const defined on top of the component TOTAL_PAGES
, this is to restrict total number of pages we want to traverse throughout application. In real-world applications, this won't be needed as the API will give you the details of total pages available.
Also, you might have notice, we have defined a state to store page number but till now, haven't used it correctly. This is because we want to change this page number from our intersection observer.
2. Adding Intersection Observer and incrementing page number
To do an infinite scroll, we need to increment page number count when last element of the list is visible to user. This will be done by intersection observer.
Our intersection observer will observe if the last element is visible or not, if it is, we will increment the page number by 1. As our useEffect will run on change in page number, the API will get called and hence we will get list of more users.
After understanding this logic, let's see the working code -
// App.js
const App = () => {
const [loading, setLoading] = useState(true);
const [allUsers, setAllUsers] = useState([]);
const [pageNum, setPageNum] = useState(1);
const [lastElement, setLastElement] = useState(null);
const observer = useRef(
new IntersectionObserver(
(entries) => {
const first = entries[0];
if (first.isIntersecting) {
setPageNum((no) => no + 1);
}
})
);
const callUser = async () => {
setLoading(true);
let response = await axios.get(
`https://randomuser.me/api/?page=${pageNum}&results=25&seed=abc`
);
let all = new Set([...allUsers, ...response.data.results]);
setAllUsers([...all]);
setLoading(false);
};
useEffect(() => {
if (pageNum <= TOTAL_PAGES) {
callUser();
}
}, [pageNum]);
useEffect(() => {
const currentElement = lastElement;
const currentObserver = observer.current;
if (currentElement) {
currentObserver.observe(currentElement);
}
return () => {
if (currentElement) {
currentObserver.unobserve(currentElement);
}
};
}, [lastElement]);
const UserCard = ({ data }) => {
return (
<div className='p-4 border border-gray-500 rounded bg-white flex items-center'>
<div>
<img
src={data.picture.medium}
className='w-16 h-16 rounded-full border-2 border-green-600'
alt='user'
/>
</div>
<div className='ml-3'>
<p className='text-base font-bold'>
{data.name.first} {data.name.last}
</p>
<p className='text-sm text-gray-800'>
{data.location.city}, {data.location.country}
</p>
<p className='text-sm text-gray-500 break-all'>
{data.email}
</p>
</div>
</div>
);
};
return (
<div className='mx-44 bg-gray-100 p-6'>
<h1 className='text-3xl text-center mt-4 mb-10'>All users</h1>
<div className='grid grid-cols-3 gap-4'>
{allUsers.length > 0 &&
allUsers.map((user, i) => {
return i === allUsers.length - 1 &&
!loading &&
pageNum <= TOTAL_PAGES ? (
<div
key={`${user.name.first}-${i}`}
ref={setLastElement}
>
<UserCard data={user} />
</div>
) : (
<UserCard
data={user}
key={`${user.name.first}-${i}`}
/>
);
})}
</div>
{loading && <p className='text-center'>loading...</p>}
{pageNum - 1 === TOTAL_PAGES && (
<p className='text-center my-10'>â¥</p>
)}
</div>
);
};
Let's understand the code in-depth.
We have defined the Intersection Observer and stored it to const observer
. The intersection observer have a callback function which accept array of all the intersecting objects. But since, we will be passing only last element to it, we are always checking the 0th entry of this array. If that element intersects means become visible, we will increment the page number.
We have added one more state lastElement
and initialised it to null
. Inside the page, we will be passing last element of the array to this state.
Hence, when the value of lastElement
state will be changed calling another useEffect (with lastElement
in dependency-array). In this useEffect, if we get value of lastElement we will pass that element to our intersection observer to observe. Our observer will then check the intersection of this element and increment the page count once this happens.
As the page number changes, the API will be called and more users will be fetched. Notice the small change we did to add these new users to existing state and avoiding duplications.
And The app will run effortlessly and you can now see infinite scroll in action!ð¥
That is it for now! If you want to see the full code for this, you can check that in my Github repository here.
Thank you so much for reading this article. Let me know your thoughts on this and you can also connect with me on Twitter or buy me a coffee if you like my articles.
*Happy coding and keep learning ð *
Top comments (13)
Why have to used this ref={setLastElement} ? isn't it supposed to be ref={observer} ?
Read about callback refs for this.
This is great, do you have a solution for infinite scroll + virtualisation? That's the holy grail I haven't been able to implement well
A lot of React libs doing that out there Jai Sandhu
Haven't found a good one that caters for different height cells and infinite scrolling
Hi Jai. This solution has an interesting take on different height cells in a virtualised list. dev.to/miketalbot/react-virtual-wi....
Oooh wow thanks that looks awesome!
Your arcticle helped a lot. Thanks for indetailed explanation. I have some suggestion.
There are 2 ways:
with 1st we need calculate the last ele, with 2nd it becomes much cleaner and simpler.
Adding YT link for those who want video explanation.
https://www.youtube.com/watch?v=8kLOvs1prEM&t=3s
Why do you use
to store a reference to an observer?
is nothing but
It is a convenience hook often used to store auto-updated refs to an elements (with its return value passed as a ref prop), or in rare cases when we need to update persistent state without causing component to re-render (by manually updating .current value)
Here you are just storing the reference, so it is more intuitive and less confusing to use
instead.
Also since you are still using ref but update it manually using "callback ref", the presence of useRef() in the code makes it harder to understand especially to a novice.
This is great. Do you have any solution for combining infinite scroll and scroll into view? I'm trying to implement into my project, which has a navigation that I can click to go to a certain section and update the active section on my navigation, and also update my navigation whenever I scroll to a section
for this hint you can make me louda
the page Number is incrementing even when the already intersected element comes down. how can we resolve this issue?
use this library to save time :)
react-query.tanstack.com/reference...