ReactJS

Implement Infinite Scrolling in React by Making a Custom Hook

Avinash Prajapati
Avinash PrajapatiJul 8, 2025

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 CaseDescription
Social media feedsFacebook, Twitter, Instagram — loads new posts while scrolling
E-commerce productsProduct grids auto-load more items (Amazon, Flipkart)
Search resultsLoads additional search results (Google Images, YouTube, etc.)
News/BlogsLoads older articles as you scroll (Medium, Dev.to)
DashboardsLarge tables and data logs

Why Not Just Use Scroll Events?

The traditional way to detect scrolling involves listening to scroll events:

1window.addEventListener('scroll', handleScroll);

This might seem easy, but it introduces several issues:

Limitations of Scroll Events:

1. Manual Calculations Required

You must calculate scroll position, total height, and offsets every time.

2. Performance Issues

Scroll events fire very frequently — especially during fast scrolling — leading to performance bottlenecks if not throttled.

3. Hard to Maintain

Complex logic is needed when dealing with nested or dynamic scroll containers.

4. 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:

1npm create vite@latest infinite-scroll-custom-hook --template react-ts
2cd infinite-scroll-custom-hook
3npm install

Step 2: Install Material UI

1npm install @mui/material @emotion/react @emotion/styled

Step 3: Create useInfiniteScroll Hook

Create a file: src/hooks/useInfiniteScroll.ts

1//useInfiniteScroll.ts
2
3import { useEffect, useRef } from "react";
4
5interface UseInfiniteScrollProps {
6  loadMore: () => void;
7  hasMore: boolean;
8}
9
10const useInfiniteScroll = ({ loadMore, hasMore }: UseInfiniteScrollProps) => {
11  const loadMoreRef = useRef<HTMLDivElement | null>(null);
12
13  useEffect(() => {
14    if (!hasMore) return;
15
16    const observer = new IntersectionObserver(([entry]) => {
17      if (entry.isIntersecting) {
18        loadMore();
19      }
20    });
21
22    if (loadMoreRef.current) {
23      observer.observe(loadMoreRef.current);
24    }
25
26    return () => observer.disconnect();
27  }, [loadMore, hasMore]);
28
29  return loadMoreRef;
30};
31
32export 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

1//App.tsx
2
3import { useState, useRef, useCallback } from "react";
4
5import useInfiniteScroll from "./hooks/useInfiniteScroll";
6
7const App = () => {
8  const allItems = useRef<string[]>(
9    Array.from({ length: 100 }, (_, i) => `Item #${i + 1}`)
10  );
11
12  const [visibleItems, setVisibleItems] = useState<string[]>(
13    allItems.current.slice(0, 20)
14  );
15  const [hasMore, setHasMore] = useState(true);
16  const [loading, setLoading] = useState(false);
17
18  const loadMoreItems = useCallback(() => {
19    if (loading || !hasMore) return;
20    setLoading(true);
21
22    setTimeout(() => {
23      setVisibleItems((prev) => {
24        const nextItems = allItems.current.slice(prev.length, prev.length + 12);
25        const updatedItems = [...prev, ...nextItems];
26
27        if (updatedItems.length >= allItems.current.length) {
28          setHasMore(false);
29        }
30
31        return updatedItems;
32      });
33      setLoading(false);
34    }, 1000);
35  }, [loading, hasMore]);
36
37  const loadMoreRef = useInfiniteScroll({ loadMore: loadMoreItems, hasMore });
38
39  return (
40    <Box>
41      <Box>
42        <Typography>
43          Infinite Scroll Demo
44        </Typography>
45        <Typography>
46          Scroll down to load more items dynamically
47        </Typography>
48      </Box>
49
50      <Box>
51        {visibleItems.map((item, index) => (
52          <Card key={index}>
53            <CardContent>
54              <Typography>
55                {item}
56              </Typography>
57              <Divider/>
58              <Typography>
59                This is a demo item rendered using infinite scroll.
60              </Typography>
61            </CardContent>
62          </Card>
63        ))}
64      </Box>
65
66      {/* Scroll*/}
67      <div ref={loadMoreRef}/>
68
69      {loading && (
70        <Box>
71          <Fade in={loading}>
72            <CircularProgress/>
73          </Fade>
74        </Box>
75      )}
76
77      {!hasMore && (
78        <Typography>
79          You’ve reached the end of the list.
80        </Typography>
81      )}
82    </Box>
83  );
84};
85
86export default App;
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

1npm 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:

FeatureWhat to Do
API PaginationTrack current page in state and pass it to the loadMore() fetch call
FilteringReset list and observer when filters change
Server SortingReset list and sort params, then refetch
Scroll ContainerPass a root option to IntersectionObserver for scrollable divs
Error HandlingShow fallback UI when fetch fails

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.





© 2026 IGNEK. All rights reserved.

Ignek on LinkedInIgnek on InstagramIgnek on FacebookIgnek on YouTubeIgnek on X