Recently, I was working on a React project where I needed to display a long list of data from an API. The challenge was obvious: loading everything at once was slowing down the page.
That’s when I decided to use infinite scrolling. Instead of loading all the data upfront, I could load small chunks as the user scrolled down.
In this tutorial, I’ll show you the exact steps I followed to build an infinite scroll component in React. I’ll cover two simple methods, one using a popular package and another using React’s built-in Intersection Observer. Both methods are easy to follow and can be applied to real-world projects.
Method 1 – Use react-infinite-scroll-component
The first method I tried was with the react-infinite-scroll-component package. It’s simple, lightweight, and works perfectly for most use cases.
Here’s the code I used:
import React, { useState, useEffect } from "react";
import InfiniteScroll from "react-infinite-scroll-component";
import axios from "axios";
export default function InfiniteScrollDemo() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
fetchData();
}, [page]);
const fetchData = async () => {
const res = await axios.get(
`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=10`
);
setItems((prev) => [...prev, ...res.data]);
};
return (
<InfiniteScroll
dataLength={items.length}
next={() => setPage(page + 1)}
hasMore={true}
loader={<h4>Loading...</h4>}
endMessage={<p>No more data to show</p>}
>
{items.map((item) => (
<div key={item.id} style={{ padding: "10px", border: "1px solid #ccc" }}>
<h4>{item.title}</h4>
<p>{item.body}</p>
</div>
))}
</InfiniteScroll>
);
}You can refer to the screenshot below to see the output.

This method uses the react-infinite-scroll-component package. I fetch data page by page and append it to the list as the user scrolls.
Method 2 – Use Intersection Observer Hook
The second method I used was with the Intersection Observer API. This doesn’t require any external package and works well for modern browsers.
Here’s the code:
import React, { useState, useEffect, useRef } from "react";
import axios from "axios";
export default function InfiniteScrollObserver() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const loaderRef = useRef(null);
useEffect(() => {
fetchData();
}, [page]);
const fetchData = async () => {
const res = await axios.get(
`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=10`
);
setItems((prev) => [...prev, ...res.data]);
};
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
setPage((prev) => prev + 1);
}
},
{ threshold: 1 }
);
if (loaderRef.current) {
observer.observe(loaderRef.current);
}
return () => {
if (loaderRef.current) {
observer.unobserve(loaderRef.current);
}
};
}, []);
return (
<div>
{items.map((item) => (
<div key={item.id} style={{ padding: "10px", border: "1px solid #ccc" }}>
<h4>{item.title}</h4>
<p>{item.body}</p>
</div>
))}
<div ref={loaderRef} style={{ height: "50px", background: "#eee" }}>
Loading more...
</div>
</div>
);
}You can refer to the screenshot below to see the output.

Here, I used the Intersection Observer API to detect when the loader div enters the viewport. When it does, the next page of data loads automatically.
Method 3 – Manual Scroll Event Listener (Not Recommended but Useful)
I also tested a more traditional way, listening to the scroll event. While it works, it’s not as efficient as the first two methods.
import React, { useState, useEffect } from "react";
import axios from "axios";
export default function InfiniteScrollManual() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
fetchData();
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [page]);
const fetchData = async () => {
const res = await axios.get(
`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=10`
);
setItems((prev) => [...prev, ...res.data]);
};
const handleScroll = () => {
if (
window.innerHeight + document.documentElement.scrollTop + 1 >=
document.documentElement.scrollHeight
) {
setPage((prev) => prev + 1);
}
};
return (
<div>
{items.map((item) => (
<div key={item.id} style={{ padding: "10px", border: "1px solid #ccc" }}>
<h4>{item.title}</h4>
<p>{item.body}</p>
</div>
))}
</div>
);
}You can refer to the screenshot below to see the output.

This method listens to the scroll event and checks when the user reaches the bottom. It’s simple but can affect performance on large pages.
Both methods work great. If you want something quick and reliable, go with react-infinite-scroll-component. If you prefer no external dependencies, use Intersection Observer.
I personally use the package for quick projects and Intersection Observer for production apps where performance is critical.
I hope you found this tutorial helpful. If you have any questions or want me to cover advanced use cases like API debouncing or error handling, let me know in the comments.
You may read:
- Props in React JS
- Which is the Best React Component Library?
- Create Tables in React Using react-data-table-component
- Pass a Component as a Prop in React

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.