Infinite scrolling is a powerful technique that improves user experience by loading content dynamically. In this guide, we’ll walk through three different ways to implement infinite scrolling in React, including both custom-built and library-based solutions.
Update history:
The most common method to implement infinite scroll is to use prebuilt React libraries such as react-infinite-scroll-component and react-window-infinite-loader:
import InfiniteScroll from "react-infinite-scroll-component";
import { useState, useEffect } from 'react';
function App() {
const [page, setPage] = useState(1);
const [products, setProducts] = useState([]);
const [totalProducts, setTotalProducts] = useState(0);
const fetchData = async (page: number) => {
try {
const res = await fetch(
`https://dummyjson.com/products/?limit=10&skip=${(page - 1) * 10}`
);
const data = await res.json();
if (res.ok) {
setProducts((prevItems) => [...prevItems, ...data.products]);
page === 1 && setTotalProducts(() => data.total);
}
} catch (error) {
console.log(error)
}
};
useEffect(() => {
let subscribed = true;
(async () => {
if (subscribed) {
await fetchData(1);
}
})();
return () => {
subscribed = false;
};
}, []);
const handleLoadMoreData = () => {
setPage((prevPage) => {
const nextPage = prevPage + 1;
fetchData(nextPage);
return nextPage;
});
};
return (
<InfiniteScroll
dataLength={products.length}
next={handleLoadMoreData}
hasMore={totalProducts > products.length}
loader={<p>Loading...</p>}
endMessage={<p>No more data to load.</p>}
>
<div>
{products.map((product) => (
<div key={product.id}>
<h2>
{product.title} - {product.id}
</h2>
</div>
))}
</div>
</InfiniteScroll>
);
}
However, there are multiple ways to achieve the infinite scroll effect. In this guide, we’ll explore three unique approaches to implementing infinite scrolling in React applications:
react-infinite-scroll-component and react-window-infinite-loader, which helps save time and effort while still offering customization optionsThe Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
Infinite scroll eliminates the need for traditional pagination. Instead of navigating through many pages, users can scroll nonstop to view more content, making the experience more engaging and intuitive.
Infinite scroll is widely used in social media platforms like Instagram, X, and TikTok, enabling users to endlessly browse through feeds of images and videos without interruption.
Now that we’ve established what infinite scroll is, let’s proceed with the tutorial.
First we would set up a foundation that will be consistently implemented across all the various infinite scrolling techniques.
To get started, let’s first set up a React application using Vite. In your terminal, run the following commands:
npm create vite@latest ecommerce-app -- --template react-ts cd ecommerce-app npm i
The commands above will generate a React application in TypeScript and install all dependencies. Next, we’ll set up the initial state for our component in App.tsx. This includes the list of items to display, the necessary loading and error indicators, and a variable to track the total products available:
// App.tsx
import React, { useState, useEffect } from 'react';
type ProductItem = {
id: number;
title: string;
description: string;
category: string;
price: number;
rating: number;
thumbnail: string;
brand: string;
discountPercentage: number;
};
function App() {
const [loading, setLoading] = useState(true);
const [products, setProducts] = useState<ProductItem[]>([]);
const [totalProducts, setTotalProducts] = useState(0);
const [error, setError] = useState<null|Error>(null);
// rest of component
}
Next, we’ll create a function to fetch data from an API, increment the page number, and update the state with the fetched items. Additionally, we’ll handle any errors during the data fetching process:
const fetchData = async (page: number) => {
try {
setLoading(true);
const res = await fetch(
`https://dummyjson.com/products/?limit=10&skip=${(page - 1) * 10}`
);
const data = await res.json();
if (res.ok) {
setProducts((prevItems) => [...prevItems, ...data.products]);
page === 1 && setTotalProducts(() => data.total); // only set this once
}
setLoading(false);
} catch (error) {
setLoading(false);
if (error instanceof Error) {
setError(error);
}
}
};
For this tutorial, we will be using the DummyJSON products API. DummyJSON doesn’t offer an explicit page param. Instead, it uses limit and skip for rendering pagination. limit is the maximum number of products we want per API call, and skip is the number of items we intend to skip for each page, which in our case would be the previous page multiplied by 10.
fetchData on component mountNext, we’ll use the useEffect Hook to call the fetchData function when the component mounts initially:
useEffect(() => {
let subscribed = true;
(async () => {
if (subscribed) {
await fetchData(1);
}
})();
return () => {
subscribed = false;
};
}, []);
In a useEffect hook, we want to make sure we clean up the asynchronous function call to avoid state updates when the component has unmounted. AbortController is another method of unsubscribing from a fetch call. You can learn more on how to clean up React’s useEffect hook.
ProductCard componentFinally, we will create the ProductCard component that would be used for each product and the CSS styles it needs. So go ahead and create a component directory inside src. In that, create a ProductCard.tsx file and paste the code below:
export const ProductCard = ({ product }: { product: ProductItem }) => {
const discountedPrice = (
product.price -
(product.price * product.discountPercentage) / 100
).toFixed(2);
return (
<div className="product-card">
<img
src={product.thumbnail}
alt={product.title}
className="product-image"
/>
<div className="product-info">
<h2 className="product-title">
{product.title} - {product.id}
</h2>
<span className="product-category">{product.category}</span>
{product.brand && (
<span className="product-brand">{product.brand}</span>
)}
<p className="product-description">{product.description}</p>
<div className="product-props">
<div className="product-price">
${discountedPrice}
<span className="product-original-price">
${product.price.toFixed(2)}
</span>
</div>
<div className="product-rating">
<span className="star-rating">{"★"}</span>
<span>{Math.floor(product.rating)}</span>
</div>
</div>
<button className="add-to-cart">Add to Cart</button>
</div>
</div>
);
};
The Vite template we used in creating the React app comes with two CSS files by default, but we only need one. Delete the App.css and its import in App.tsx. In index.css, replace the styles there with the ones below:
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap");
.App {
font-family: "Poppins", sans-serif;
}
.products-list {
display: grid;
grid-template-columns: 1fr;
grid-gap: 10px;
max-width: 768px;
margin: 0 auto;
}
@media screen and (min-width: 768px) {
.products-list {
grid-template-columns: 1fr 1fr;
}
}
.product-card {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
max-width: 400px;
width: 100%;
overflow: hidden;
font-family: "Poppins", sans-serif;
}
.product-image {
width: 100%;
height: 250px;
object-fit: cover;
}
.product-info {
padding: 20px;
}
.product-title {
font-size: 24px;
margin: 0 0 10px;
}
.product-category,
.product-brand {
display: inline-block;
background-color: #e0e0e0;
padding: 5px 10px;
border-radius: 15px;
font-size: 12px;
margin-right: 5px;
}
.product-description {
font-size: 14px;
color: #666;
margin: 10px 0;
}
.product-props {
display: flex;
justify-content: space-between;
align-items: center;
}
.product-price {
font-size: 24px;
font-weight: bold;
margin: 10px 0;
}
.product-original-price {
text-decoration: line-through;
color: #999;
font-size: 16px;
margin-left: 10px;
}
.product-rating {
display: flex;
align-items: center;
margin: 10px 0;
}
.star-rating {
color: #ffd700;
font-size: 18px;
margin-right: 5px;
}
.add-to-cart {
display: block;
width: 100%;
padding: 10px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
margin-top: 20px;
}
.add-to-cart:hover {
background-color: #45a049;
}
These foundational steps will be present in all the techniques we discuss in this article. We’ll modify and expand upon them as a base.
Building the entire infinite scroll implementation from scratch involves handling the scroll event, loading more data, and updating the state in your React application. This approach provides you with full control over customization and functionality.
To get started, create a component FromScratch.tsx in the components directory, and initialize:
import { useEffect, useState } from "react";
import { ProductCard } from "./ProductCard";
import { ProductItem } from "../types";
export const FromScratch = ({
products,
fetchData,
loading,
error
}: {
products: ProductItem[];
fetchData: (page: number) => Promise;
loading: boolean;
error: null|Error
}) => {
const [page, setPage] = useState(1);
// scroll logic
return (
<div>
<div className="products-list">
{products.map((product, index) => (
<ProductCard product={product} key={index} />
))}
</div>
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
</div>
);
};
Next, we’ll create a function to handle the scroll event. This function will check if the user has reached the bottom of the page and call fetchData if necessary. We’ll add a scroll event listener to the window object and remove it when the component is unmounted. In place of the // scroll logic comment, add this:
const handleScroll = () => {
const bottom =
Math.ceil(window.innerHeight + window.scrollY) >=
document.documentElement.scrollHeight - 200;
if (bottom) {
setPage((prevPage) => {
const nextPage = prevPage + 1;
fetchData(nextPage);
return nextPage;
});
}
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
If you’re wondering why the fetchData is called inside the setPage callback, it is simply because the fetchData function is a side effect of the page state. With this we’re making sure the fetchData has access to the most recent page state.
React’s state updates are asynchronous, so relying on the updated state immediately after calling the setState function can lead to race conditions. Another alternative is to use the useEffect hook to track when the page has been updated, but in this case, we don’t have that luxury.
Next, let’s import our newly created component into App.tsx and run the app:
// App.tsx
<div>
<FromScratch
products={products}
fetchData={fetchData}
loading={loading}
error={error}
/>
</div>
You can run the app with:
npm run dev
And in your browser you should have something like this:

If you’re not creating along with this tutorial, then you can follow up with the Codesandbox app.
Our handleScroll function performs a check for the bottom of the page every time the user scrolls, which we don’t want. Instead, we only want to check for the bottom when the user stops scrolling. Debouncing in React in our case creates a cooling period where it waits until the user has stopped scrolling before executing our check function:
// FromScratch.tsx
const debounce = (func: (args: any) => void, delay: number) => {
let timeoutId: ReturnType<typeof setTimeout>;
return function (...args: any) {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
func(args);
}, delay);
};
};
Next we’ll update the handleScroll to make use of the debounce function:
const handleScroll = debounce(() => {
const bottom =
Math.ceil(window.innerHeight + window.scrollY) >=
document.documentElement.scrollHeight - 200;
if (bottom) {
setPage((prevPage) => {
const nextPage = prevPage + 1;
fetchData(nextPage);
return nextPage;
});
}
}, 300);
The purpose of clearing the timeout in the debounce function is to cancel previous timeouts when scrolling resumes.

In a full-scale application, you’d most likely need infinite scroll in multiple pages; in this section we’ll make the scroll function a reusable custom hook. So create a hooks directory in src and create a useInfiniteScroll.ts file:
import { useEffect, useState } from "react";
const debounce = (func: (args: any) => void, delay: number) => {
let timeoutId: ReturnType<typeof setTimeout>;
return function (...args: any) {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
func(args);
}, delay);
};
};
export const useInfiniteScroll = (fetchData: (page: number) => Promise<void>) => {
const [page, setPage] = useState(1);
const handleScroll = debounce(() => {
const bottom =
Math.ceil(window.innerHeight + window.scrollY) >=
document.documentElement.scrollHeight - 200;
if (bottom) {
setPage((prevPage) => {
const nextPage = prevPage + 1;
fetchData(nextPage);
return nextPage;
});
}
}, 300);
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
};
Now in FromScratch.tsx we can replace the scroll logic with:
export const FromScratch = ({
products,
fetchData,
loading,
error,
}: {
products: ProductItem[];
fetchData: (page: number) => Promise<void>;
loading: boolean;
error: null | Error;
}) => {
useInfiniteScroll(fetchData);
// rest of component
};
With that, we have a fully functional infinite scroll implementation built from scratch. This approach allows for extensive customization and more control over functionality. However, it may be more time-consuming and, as we’ve seen, requires more maintenance than using an existing library or component.
Using an existing infinite scroll library or component can save time and effort as you leverage pre-built and pre-tested solutions while retaining customization options. We will cover two of these libraries in this section.
react-infinite-scroll-component is a popular library for implementing infinite scrolling in React. Let’s learn how to use this library to create infinite scrolling in our e-commerce application. First, install react-infinite-scroll-component:
npm install react-infinite-scroll-component
Now we can create a new component within the components directory, we’ll call it WithReactScroll.tsx. We’ll import the InfiniteScroll component from the library, and wrap the list of products in it. Configure the component by passing the necessary props like dataLength, next, hasMore, and loader:
import InfiniteScroll from "react-infinite-scroll-component";
import { ProductCard } from "./ProductCard";
import { useState } from "react";
export const WithReactScroll = ({
products,
fetchData,
totalProducts,
}: {
products: ProductItem[];
fetchData: (page: number) => Promise<void>;
totalProducts: number;
}) => {
const [page, setPage] = useState(1);
const handleLoadMoreData = () => {
setPage((prevPage) => {
const nextPage = prevPage + 1;
fetchData(nextPage);
return nextPage;
});
};
return (
<InfiniteScroll
dataLength={products.length}
next={handleLoadMoreData}
hasMore={totalProducts > products.length}
loader={<p>Loading...</p>}
endMessage={<p>No more data to load.</p>}
>
<div className="products-list">
{products.map((item) => (
<ProductCard product={item} key={item.id} />
))}
</div>
</InfiniteScroll>
);
};
Now we can import the WithReactScroll component into our App component to see the result:
<div>
<WithReactScroll
products={products}
fetchData={fetchData}
totalProducts={totalProducts}
/>
</div>
You can view the result on Codesandbox.
With that, we’ve implemented infinite scrolling in our React application. We didn’t use the window’s scroll event because react-infinite-scroll-component handles that for us. The react-infinite-scroll-component library offers a faster and more streamlined implementation process but still provides customization options, like scroll height and scroll overflow. However, you should keep in mind the trade-off of introducing additional dependencies to your project.
Second on our list is the react-window library, which was designed for rendering large lists efficiently, and the react-window-infinite-loader library, which is used to handle infinite scrolling and load more data as the user scrolls. First, we’ll install the react-window-infinite-loader and react-window library:
npm install react-window-infinite-loader react-window
Next, we’ll create a WithReactWindow component in the components directory:
import { useState } from "react";
import { FixedSizeList as List } from "react-window";
import InfiniteLoader from "react-window-infinite-loader";
import { ProductCard } from "./ProductCard";
export const WithReactWindow = ({
fetchData,
products,
totalProducts,
loading,
}: {
products: ProductItem[];
fetchData: (page: number) => Promise<void>;
totalProducts: number;
loading: boolean;
}) => {
const [page, setPage] = useState(1);
const hasNextPage = totalProducts > products.length;
const handleLoadMoreData = () => {
if (loading) return;
setPage((prevPage) => {
const nextPage = prevPage + 1;
fetchData(nextPage);
return nextPage;
});
};
const isItemLoaded = (index: number) => !hasNextPage || index < products.length;
const Row = ({ index, style }: { index: number, style: { [key:string]:any } }) => {
return (
<div style={style}>
{isItemLoaded(index) ? (
<ProductCard product={products[index]} />
) : (
"Loading..."
)}
</div>
);
};
return (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={hasNextPage ? products.length + 1 : products.length}
loadMoreItems={handleLoadMoreData}
>
{({ onItemsRendered, ref }) => (
<List
height={window.innerHeight}
itemCount={products.length}
itemSize={600}
onItemsRendered={onItemsRendered}
ref={ref}
width={450}
>
{Row}
</List>
)}
</InfiniteLoader>
);
};
In the code above, when we combine InfiniteLoader() and FixedSizeList(), the component ensures that only the visible items are rendered and new items are loaded as the user scrolls downwards continuously, creating the infinite scroll feature.
Now we can import the component into App to see the result:
<div>
<WithReactWindow
products={products}
fetchData={fetchData}
totalProducts={totalProducts}
loading={loading}
/>
</div>
You can also view the result on Codesandbox.
The Intersection Observer API is a modern development technique that can detect when elements come into view, thereby triggering content loading for infinite scrolling. The Intersection Observer API observes changes in the intersection of target elements with an ancestor element or the viewport, making it well-suited to implement infinite scrolling.
We’ll create a new component inside the components directory, we can call it WithIntersectionObserver. Next, we’ll create a ref for the observer target element and set up the Intersection Observer in a useEffect Hook. When the target element comes into view, call the fetchData function as follows:
import { useEffect, useRef, useState } from "react";
import { ProductCard } from "./ProductCard";
export const WithIntersectionObserver = ({
products,
fetchData,
error,
loading
}: {
products: ProductItem[];
fetchData: (page: number) => Promise<void>;
error: null|Error,
loading: boolean
}) => {
const [page, setPage] = useState(1);
const observerTarget = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
setPage((prevPage) => {
const nextPage = prevPage + 1;
fetchData(nextPage);
return nextPage;
});
}
},
{ threshold: 1 }
);
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => {
if (observerTarget.current) {
observer.unobserve(observerTarget.current);
}
};
}, [observerTarget]);
// rest of component
};
Then, render the items, loading indicator, error messages, and the observer target element within the component:
return (
<>
<div className="products-list">
{products.map((product) => (
<ProductCard product={product} key={product.id} />
))}
</div>
<div ref={observerTarget}></div>
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
</>
);
By leveraging the Intersection Observer API, we have created an efficient and performant infinite scrolling solution in our React application. This approach offers a modern, browser-native method for detecting when elements come into view, but it may not be supported in old browsers and environments without using a polyfill.
Next, we’ll import the new component into App to see the results:
//App.tsx
<div>
<WithIntersectionObserver
products={products}
fetchData={fetchData}
loading={loading}
error={error}
/>
</div>
You can also view the results on Codesandbox.
We’ve explored three different ways to implement infinite scroll in React. Since you’ll likely only need one for your application, choosing the right method depends on your app’s specific requirements. Here are some key considerations to help you decide:
Let’s break down the pros and cons of each method:
This approach gives you full control over the implementation and allows for unlimited customization. However, it requires more effort to optimize performance (e.g., debouncing scroll events, efficiently updating the DOM) and can be prone to bugs if not implemented carefully.
Use this method if:
Libraries provide a quick and reliable way to implement infinite scroll, often with built-in optimizations and community support. However, they can add bloat to your app, and you’ll rely on the library’s maintainers for updates and bug fixes.
Use this method if:
This modern approach eliminates the need for scroll event listeners and debouncing, making it more performant and easier to maintain. However, it’s not fully supported in older browsers like Internet Explorer.
Use this method if:
ref and scrollTop APIScroll to top is an additional functionality often implemented in infinite scrolling that provides a better user experience. If you’ve been on social media anytime recently, you’re familiar with scroll to top.
In X, for example, when you scroll through your for you page (FYP), it never really ends; this is an example of infinite scrolling in action. Then, when you click the home icon in the X navigation menu, it takes you right back to the top. The home icon in X serves two purposes: to refresh and fetch more data from your FYP and to provide modern scroll to top functionality.
For a good user experience, I think all implementations of infinite scroll should have the option to scroll back up to the top of the feed. To implement this in React, we will need the scrollTop() property and the useRef() Hook to have good control of the scroll position.
Both of them are a strong mix that comes in handy when implementing features like the scroll to top buttons or dynamically loading content as the user scrolls, as seen in the example where we built the entire implementation from scratch. We will implement it in our App.tsx file:
import React, { useRef } from 'react';
function App() {
const scrollableDiv = useRef<HTMLDivElement | null>(null);
// scroll logic</span>
const scrollToTop = () => {
if (scrollableDiv.current) {
scrollableDiv.current.scrollTop = 0;
}
};
return (
<div ref={scrollableDiv}>
<!-- Infinite Scroll content -->
<button onClick={scrollToTop}>Scroll to Top</button>
</div>
);
}
export default ScrollableComponent;
In the code above, the scroll to the top is achieved using a ref and scrollTop(), which directly accesses and manipulates the scroll position of the div through the scrollToTop() function. With this, we can easily improve our user’s infinite scrolling experience.
Infinite scrolling is a powerful web design technique. It enhances the user experience by progressively loading content as users scroll down a page, thereby eliminating the need for pagination. In this article, we explored four different approaches for implementing infinite scrolling in React applications by building an e-commerce products page.
Each technique has its advantages and potential drawbacks, so it’s essential to choose the method that best suits your specific requirements and your users’ needs. By implementing infinite scrolling in your React applications, you can provide an intuitive and engaging user experience that keeps visitors engaged with your content. I hope you enjoyed this article! Be sure to leave a comment if you have any questions.
Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not
server-side
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
// Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>

Promise.all still relevant in 2025?In 2025, async JavaScript looks very different. With tools like Promise.any, Promise.allSettled, and Array.fromAsync, many developers wonder if Promise.all is still worth it. The short answer is yes — but only if you know when and why to use it.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 29th issue.

Learn about the new features in the Next.js 16 release: why they matter, how they impact your workflow, and how to start using them.

Test out Meta’s AI model, Llama, on a real CRUD frontend projects, compare it with competing models, and walk through the setup process.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now
2 Replies to "3 ways to implement infinite scroll in React (with code examples)"
Hi,
Thanks,
I followed you & implemented the infinite scroll. I have been facing this issue.
I have set page size 30, So for each API response I receive the 30 data. The problem is, when I open the application in big screen like TV/projector, Initial API call loads the 30 data & no scroll appear due to large screen. But I have 1000 data in Database.
I told team, We could increase the initial page size 100, then they asked suppose after increased size What If the scroll doesn’t appear on window. More over, they want to keep page size 30 only, do not want to change.
How to solve when I open the application in big screen, load the data until scroll appear?
This error occured because you did not specify the context in which the infinite scroll should work. It defaults to the viewport if not set.
The solution is that you pass in the root which is a selector to the options object as shown below.
let options = {
root: document.querySelector(“#scrollArea”),
rootMargin: “0px”,
threshold: 1.0,
};
let observer = new IntersectionObserver(callback, options);