How to implement infinite scroll in React

Implementing infinite scroll provides a seamless browsing experience for React applications with large datasets by loading content automatically. As the creator of CoreUI with over 11 years of React development experience since 2014, I’ve built infinite scroll features in countless social feeds and product listings. The most effective solution is to use the Intersection Observer API to detect when users reach the bottom and load more data. This approach is performant, doesn’t require scroll event listeners, and provides smooth user experience.

Use Intersection Observer API to implement infinite scroll in React.

const [items, setItems] = useState([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [loading, setLoading] = useState(false)
const observerRef = useRef()

const loadMore = async () => {
  if (loading || !hasMore) return

  setLoading(true)
  try {
    const response = await fetch(`/api/items?page=${page}&limit=20`)
    const newItems = await response.json()

    if (newItems.length === 0) {
      setHasMore(false)
    } else {
      setItems(prev => [...prev, ...newItems])
      setPage(prev => prev + 1)
    }
  } catch (error) {
    console.error('Failed to load items:', error)
  } finally {
    setLoading(false)
  }
}

const lastItemRef = useCallback(node => {
  if (loading) return
  if (observerRef.current) observerRef.current.disconnect()

  observerRef.current = new IntersectionObserver(entries => {
    if (entries[0].isIntersecting && hasMore) {
      loadMore()
    }
  })

  if (node) observerRef.current.observe(node)
}, [loading, hasMore])

return (
  <div>
    {items.map((item, index) => {
      if (items.length === index + 1) {
        return <div ref={lastItemRef} key={item.id}>{item.name}</div>
      }
      return <div key={item.id}>{item.name}</div>
    })}

    {loading && <div>Loading more...</div>}
    {!hasMore && <div>No more items</div>}
  </div>
)

The Intersection Observer watches the last item in the list. When it becomes visible, loadMore() fetches the next page. The observer is recreated when loading or hasMore changes. The ref callback pattern attaches the observer to the last element dynamically as the list grows. State tracks loading status and whether more items exist.

Best Practice Note

This is the same infinite scroll implementation we use in CoreUI React components for optimal performance. The Intersection Observer is more efficient than scroll event listeners as it doesn’t trigger on every scroll. Add error handling and retry logic for failed requests, and consider adding a manual “Load More” button as a fallback.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author