Introduction
Infinite scrolling is a widely used UX pattern to load data as the user scrolls, removing the need for pagination and creating a seamless browsing experience — especially in feeds, search results, and product listings.
In this blog, we will :
- Understand when infinite scrolling is useful.
- Learn why the Intersection Observer API is a better approach than scroll events.
- Set up a React + TypeScript + Vite project.
- Use Material UI (MUI) for UI components.
- Build a reusable custom hook useInfiniteScroll.
- Integrate infinite scroll into a working component.
What Is Infinite Scrolling?
Infinite scrolling dynamically loads more content as the user scrolls down the page, instead of requiring them to click on pagination buttons like “Next” or “Page 2.”
Common Use Cases :
Use Case | Description |
Social media feeds | Facebook, Twitter, Instagram — loads new posts while scrolling |
E-commerce products | Product grids auto-load more items (Amazon, Flipkart) |
Search results | Loads additional search results (Google Images, YouTube, etc.) |
News/Blogs | Loads older articles as you scroll (Medium, Dev.to) |
Dashboards | Large tables and data logs |
Why Not Just Use Scroll Events?
The traditional way to detect scrolling involves listening to scroll events :
window.addEventListener('scroll', handleScroll);
This might seem easy, but it introduces several issues :
Limitations of Scroll Events :
- Manual Calculations Required
You must calculate scroll position, total height, and offsets every time. - Performance Issues
Scroll events fire very frequently — especially during fast scrolling — leading to performance bottlenecks if not throttled. - Hard to Maintain
Complex logic is needed when dealing with nested or dynamic scroll containers. - Unreliable with Dynamic Content
Layout changes or lazy-loaded components can cause scroll detection to fail.
Why Use the Intersection Observer API?
The Intersection Observer API is a modern, browser-native feature that lets you observe when an element enters the viewport — without expensive scroll checks.
Advantages :
Efficient and performs better than scroll events
No manual calculations of scroll position
Works with both window scroll and scrollable containers
Automatically cleans up when the element is removed
So for this blog, we’ll build our infinite scroll using this powerful and elegant approach.
Step 1: Set Up React + TypeScript with Vite
Run the following commands to initialize your project:
npm create vite@latest infinite-scroll-custom-hook --template react-ts
cd infinite-scroll-custom-hook
npm install
Step 2 : Install Material UI
npm install @mui/material @emotion/react @emotion/styled
Step 3 : Create useInfiniteScroll Hook
Create a file: src/hooks/useInfiniteScroll.ts.
//useInfiniteScroll.ts
import { useEffect, useRef } from "react";
interface UseInfiniteScrollProps {
loadMore: () => void;
hasMore: boolean;
}
const useInfiniteScroll = ({ loadMore, hasMore }: UseInfiniteScrollProps) => {
const loadMoreRef = useRef(null);
useEffect(() => {
if (!hasMore) return;
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
loadMore();
}
});
if (loadMoreRef.current) {
observer.observe(loadMoreRef.current);
}
return () => observer.disconnect();
}, [loadMore, hasMore]);
return loadMoreRef;
};
export default useInfiniteScroll;
useInfiniteScroll Hook :
This custom hook uses the Intersection Observer API to detect when a hidden div (placed at the bottom of the list) comes into view. When it becomes visible, it automatically calls the loadMore function to fetch more items — as long as hasMore is true.
It returns a ref (loadMoreRef) that you attach to that bottom div, making infinite scrolling smooth and efficient without manual scroll tracking.
Step 4 : Implement in App.tsx
//App.tsx
import { useState, useRef, useCallback } from "react";
import useInfiniteScroll from "./hooks/useInfiniteScroll";
const App = () => {
const allItems = useRef(
Array.from({ length: 100 }, (_, i) => `Item #${i + 1}`)
);
const [visibleItems, setVisibleItems] = useState(
allItems.current.slice(0, 20)
);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
const loadMoreItems = useCallback(() => {
if (loading || !hasMore) return;
setLoading(true);
setTimeout(() => {
setVisibleItems((prev) => {
const nextItems = allItems.current.slice(prev.length, prev.length + 12);
const updatedItems = [...prev, ...nextItems];
if (updatedItems.length >= allItems.current.length) {
setHasMore(false);
}
return updatedItems;
});
setLoading(false);
}, 1000);
}, [loading, hasMore]);
const loadMoreRef = useInfiniteScroll({ loadMore: loadMoreItems, hasMore });
return (
Infinite Scroll Demo
Scroll down to load more items dynamically
{visibleItems.map((item, index) => (
{item}
This is a demo item rendered using infinite scroll.
))}
{/* Scroll*/}
{loading && (
)}
{!hasMore && (
You’ve reached the end of the list.
)}
);
};
export default App;
- Initial Setup
- A list of 100 items is created and stored in a ref (allItems). The full list is stored using useRef instead of useState because this data is static and does not change during the component’s lifecycle. Using useRef allows the data to persist across renders without causing re-renders, making it more efficient for holding stable, non-reactive data.
- The component shows the first 20 items using visibleItems state.
- State Management
- visibleItems: Holds the items currently visible on screen.
- hasMore: Tracks if there are more items to load.
- loading: Prevents multiple simultaneous load operations.
- Load More Items Function (loadMoreItems)
- A useCallback function that runs when the user scrolls to the bottom.
- It adds 12 more items to the visibleItems list after a 1-second delay (to simulate loading).
- If all items have been loaded, it sets hasMore to false to stop further loading.
- Infinite Scroll Hook (useInfiniteScroll)
- Returns a ref (loadMoreRef) that is placed at the bottom of the page.
- When this ref becomes visible (i.e., user scrolls near the end), the hook automatically calls loadMoreItems — but only if hasMore is true.
- Loading & Completion Handling
- While loading is in progress, a spinner is shown.
- Once all items are loaded, a message is displayed indicating the end of the list.
Step 5 : Start the App
npm run dev
Visit the local host in your browser. Scroll to the bottom and watch items load automatically.
Real-World Enhancements
Here’s how you can extend this setup in real-world apps :
Feature | What to Do |
API Pagination | Track current page in state and pass it to the loadMore() fetch call |
Filtering | Reset list and observer when filters change |
Server Sorting | Reset list and sort params, then refetch |
Scroll Container | Pass a root option to IntersectionObserver for scrollable divs |
Error Handling | Show fallback UI when fetch fails |
Output
CONCLUSION
If you’re building infinite scrolling in a React app, using a custom hook with the Intersection Observer API is usually the cleaner choice. It helps you avoid attaching scroll listeners directly, which can lead to messy code and performance issues. Plus, it’s reusable and keeps things organized.
That being said, infinite scroll doesn’t always cover every use case. In situations where users want to jump to a page, apply filters, or sort the data, traditional pagination might work better. The right approach depends on what kind of data you’re dealing with and what the user expects.
Explore Our Services
Discover how we can help your business thrive, whether you’re running a small startup, an SME, or a large enterprise. We’re here to understand your unique needs and goals, offering the expertise and resources to support your journey to success.
Stay informed about our ReactJS services and updates by subscribing to our newsletter—just fill in the details below to subscribe.