Infinite scrolling in React is a modern UI technique that lets users browse large sets of data by simply scrolling, as fresh content loads automatically when they reach the end of the list. This approach is everywhere: from your social media feed to product catalogues and streaming platforms.
By removing the need for page clicks, infinite scroll helps build seamless, immersive, and highly engaging user experiences, especially on mobile devices, where natural scrolling is king. But behind this magic, React developers need to carefully handle data fetching, rendering performance, loading indicators, and accessibility to get it right.
What is Infinite Scrolling and How Does It Work?
Infinite scrolling is a web technique where content loads continuously as the user scrolls down. Instead of clicking "Next" or moving through pages (pagination), new items appear automatically at the page’s bottom. This seamless experience hooks users into exploring more content, keeps engagement high, and is ideal for social feeds, galleries, and discovery-driven apps.
Infinite Scroll vs Pagination
Infinite scrolling boosts engagement, but it can suffer from SEO and navigation issues. Pagination is best for content-heavy or search-centric sites.
Implementing Infinite Scroll in React
Using Intersection Observer API
The Intersection Observer API tracks when an element (e.g., a loader) enters the viewport, triggering new data loads:
import { useRef, useEffect } from "react";
function InfiniteScrollList({ fetchMore }) {
const loaderRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) fetchMore();
});
if (loaderRef.current) observer.observe(loaderRef.current);
return () => observer.disconnect();
}, [fetchMore]);
return (
{/* ...list items... */}
Loading...
);
}
This is efficient, less error-prone than scroll listeners, and great for both window and div scroll containers.
Using 'react-infinite-scroll-component' Library
react-infinite-scroll-component lets you drop in infinite scrolling with minimal effort:
import InfiniteScroll from 'react-infinite-scroll-component';
<InfiniteScroll
dataLength={items.length}
next={fetchMore}
hasMore={hasMore}
loader={<h4>Loading...</h4>}
>
{items.map(item => <Item key={item.id} {...item} />)}
</InfiniteScroll>
It handles pagination, loading states, and bottom triggers for you.
Custom Infinite Scroll with useEffect, useRef, and useState
This approach allows you to create your own infinite scroll logic without relying on libraries. It focuses on the window scroll position and loads new data when the user scrolls near the bottom.
Step-by-step Example:
import React, { useEffect, useState, useRef } from "react";
import axios from "axios";
function InfiniteScrollList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const loadMoreRef = useRef();
// Fetch data function
const fetchItems = async () => {
setLoading(true);
try {
const res = await axios.get(`/api/items?page=${page}`);
setItems((prev) => [...prev, ...res.data.items]);
setHasMore(res.data.hasMore); // Assume API tells if there's more
} catch (err) {
// Handle errors
}
setLoading(false);
};
// Observe the loader at bottom of list
useEffect(() => {
if (loading || !hasMore) return;
const observer = new window.IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
setPage((prev) => prev + 1);
}
},
{ threshold: 1 }
);
if (loadMoreRef.current) observer.observe(loadMoreRef.current);
return () => observer.disconnect();
}, [loading, hasMore]);
// Fetch next page whenever page state changes
useEffect(() => {
if (hasMore) fetchItems();
}, [page]);
return (
<div>
{items.map((item) => (
<div key={item.id}>{item.title}</div>
))}
{loading && <div>Loading...</div>}
<div ref={loadMoreRef} style={{ height: 1 }}></div>
</div>
);
}
export default InfiniteScrollList;
- useState tracks items, loading, page, and hasMore for pagination.
- useRef targets a “loader” div at the end of the list.
- useEffect sets up an Intersection Observer to detect when the loader is visible, then increments the page.
- The second useEffect fetches new data when the page updates.
- Always clean up observers to avoid memory leaks.
Infinite Scrolling with Virtualized Lists ('react-window', 'react-virtualized')
When your app needs to render thousands (or even millions) of items, think chat threads, product grids, or feeds. A standard infinite scroll can choke memory and performance. Virtualization solves this by only rendering the items actually visible on screen, replacing off-screen content with lightweight placeholders.
Example: Basic Virtualized List with react-window
import React from "react";
import { FixedSizeList as List } from "react-window";
const Row = ({ index, style }) => (
<div style={style}>
Row {index}
</div>
);
const MyList = ({ data }) => (
<List
height={400}
itemCount={data.length}
itemSize={35}
width={"100%"}
>
{Row}
</List>
);
- Only the visible rows are mounted in the DOM.
- This keeps your app fast even with huge datasets.
import { VariableSizeList as List } from "react-window";
const getItemSize = index => data[index].height;
<List
height={500}
itemCount={data.length}
itemSize={getItemSize}
width={400}
>
{Row}
</List>
- Dynamically calculates each row’s height.
Infinite Scroll With 'react-virtualized'
import { List } from "react-virtualized";
const rowRenderer = ({ index, key, style }) => (
<div key={key} style={style}>
Row {index}
</div>
);
<List
width={300}
height={300}
rowCount={1000}
rowHeight={20}
rowRenderer={rowRenderer}
/>
- react-virtualized also supports tables and grids, with more built-in features for complex UIs.
Why Use Virtualized Lists?
- Renders hundreds or thousands of rows with minimal DOM and memory cost.
- Keeps infinite scroll fast, even as data grows.
- Pairs perfectly with lazy loading: as you fetch new data, append it to the existing data and let virtualisation handle the heavy lifting.
Fetching Paginated API Data with Infinite Scroll
async function fetchMore() {
const res = await fetch(`/api/feed?page=${nextPage}`);
const data = await res.json();
setItems(items => [...items, ...data.items]);
}
Handling API Errors and Retries
if (error) return <button onClick={retryFunc}>Retry</button>;
Performance Optimization (Debouncing & Throttling)
- Debounce: Wait for scrolling to “pause” before firing a fetch
- Throttle: Limit fetch calls to once every N ms
import _ from "lodash"; // lodash debounce
const debouncedFetch = _.debounce(fetchMore, 500);
Loading States & Skeleton UI
Show a loader or skeleton while fetching:
{isLoading && <SkeletonLoader />}
Infinite Scroll in Div vs Window
You can trigger infinite scrolling inside any scrollable container, not just the window, by observing each container’s scroll position or attaching IntersectionObservers.
Accessibility Considerations
- Announce new content for screen readers (aria-live)
- Always provide alternate navigation for long feeds, allow jump points or a “Back to Top”
- Ensure keyboard navigation works in virtual lists
React Infinite Scroll Best Practices
- Never re-render the whole list append new items to the state
- Clean up observers in useEffect return
- Use virtualization for large datasets
- Debounce/throttle API calls for performance
- Handle errors gracefully
- Avoid memory leaks by disconnecting observers
Summary
Infinite scrolling in React offers a smooth alternative to traditional pagination, keeping users engaged with continuous, dynamic content loads. By leveraging APIs like Intersection Observer, specialised libraries, and smart state management, you can tackle huge datasets efficiently while controlling memory use and minimising re-renders.
