Back to blog
May 31, 2025
4 min read

How to Implement Infinite Scrolling in React

Infinite scrolling improves user experience by loading content continuously as the user scrolls. Learn how to implement it efficiently in your React app with IntersectionObserver.

How to Implement Infinite Scrolling in React

If you’ve used social media, you’ve experienced infinite scrolling , the seamless loading of content as you scroll down a page. This design pattern is popular for good reason: it keeps users engaged, avoids pagination clicks, and offers a smooth, modern experience.

In React, infinite scroll can be implemented in multiple ways. One of the most efficient and browser-friendly methods uses the IntersectionObserver API, which allows your app to detect when a user is nearing the end of the content list and then trigger a new fetch.

In this article, we’ll break down the concept, build a working example, and provide tips for smooth performance and clean UX.


Why Use Infinite Scrolling?

Infinite scroll works best for content feeds where users consume lots of information continuously:

  • Social media timelines
  • Product listings
  • News or article feeds
  • Image galleries

Pros

  • Better engagement and time-on-page
  • Fewer clicks and context switches
  • More mobile-friendly than traditional pagination

Cons

  • Can be harder to bookmark/share specific pages
  • Might load unnecessary content and impact performance
  • Accessibility concerns if not implemented properly

That’s why it’s essential to balance usability and technical efficiency.


Setting Up the React Component

We’ll create a simple component that fetches and displays a list of items, then loads more as the user scrolls.

1. Basic Setup

Assume you have a backend or API endpoint like /api/posts?page=2.

Install axios or use fetch to retrieve data:

npm install axios

Create a PostList component:

import React, { useEffect, useState, useRef, useCallback } from 'react';
import axios from 'axios';

export default function PostList() {
const [posts, setPosts] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const loader = useRef(null);

const fetchPosts = async () => {
const res = await axios.get(`/api/posts?page=${page}`);
if (res.data.length === 0) setHasMore(false);
setPosts(prev => [...prev, ...res.data]);
};

useEffect(() => {
if (hasMore) fetchPosts();
}, [page]);

const observer = useRef();
const lastPostRef = useCallback(
node => {
if (!hasMore) return;
if (observer.current) observer.current.disconnect();

observer.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
setPage(prev => prev + 1);
}
});

if (node) observer.current.observe(node);
},
[hasMore]
);

return (
<div>
{posts.map((post, index) => {
if (index === posts.length - 1) {
return (
<div key={post.id} ref={lastPostRef}>
{post.title}
</div>
);
}
return <div key={post.id}>{post.title}</div>;
})}
{!hasMore && <p>No more posts to load.</p>}
</div>
);
}

Explanation of the Code

  • useRef: Holds a reference to the loader element and the observer instance
  • useCallback: Prevents observer from being recreated on every render
  • IntersectionObserver: Triggers when the last item is in view
  • Pagination state: Keeps track of what page to load next

The lastPostRef attaches the observer to the last item in the list. When that item enters the viewport, we increase the page number, triggering another fetch.


Best Practices

  • Debounce or throttle fetches to avoid multiple calls
  • Add loading spinners or skeleton loaders for better UX
  • Handle errors gracefully (e.g., show a retry button if fetch fails)
  • Use hasMore logic to prevent redundant API calls
  • Lazy load images to reduce resource consumption

If you’re working with libraries like React Query or SWR, you can integrate infinite scrolling logic into their data-fetching systems for better state management.


Alternatives to IntersectionObserver

If browser support is a concern (very rare today), you can use scroll event listeners , though they’re less efficient and more error-prone:

window.addEventListener('scroll', () => {
if (window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight) {
// Load more
}
});

Still, IntersectionObserver is preferred for performance and clarity.


Conclusion

Infinite scrolling provides a clean, modern interface that keeps users engaged , especially in content-heavy applications. With the IntersectionObserver API and React’s state hooks, you can create an efficient infinite loader that’s responsive, performant, and scalable.

As always, tailor the experience to your app’s goals. For data-heavy dashboards, pagination may still be the better option. But when smooth content exploration is key, infinite scroll is the tool to reach for.


Disclaimer

Article written with the help of AI.

Read the full Disclaimer HERE