javascript 45 lines · 6 steps

Building an infinite scroll hook in React

A custom hook that fetches pages on demand and triggers the next load when a sentinel element scrolls into view.

Explained by highlit
1import { useCallback, useEffect, useRef, useState } from 'react';
2 
3export function useInfiniteScroll(fetchPage) {
4 const [items, setItems] = useState([]);
5 const [page, setPage] = useState(1);
6 const [hasMore, setHasMore] = useState(true);
7 const [loading, setLoading] = useState(false);
8 const observer = useRef(null);
9 
10 useEffect(() => {
11 let cancelled = false;
12 setLoading(true);
13 fetchPage(page)
14 .then((batch) => {
15 if (cancelled) return;
16 setItems((prev) => [...prev, ...batch.items]);
17 setHasMore(batch.hasMore);
18 })
19 .finally(() => {
20 if (!cancelled) setLoading(false);
21 });
22 return () => {
23 cancelled = true;
24 };
25 }, [page, fetchPage]);
26 
27 const sentinelRef = useCallback(
28 (node) => {
29 if (loading) return;
30 if (observer.current) observer.current.disconnect();
31 observer.current = new IntersectionObserver(
32 ([entry]) => {
33 if (entry.isIntersecting && hasMore) {
34 setPage((prev) => prev + 1);
35 }
36 },
37 { rootMargin: '200px' }
38 );
39 if (node) observer.current.observe(node);
40 },
41 [loading, hasMore]
42 );
43 
44 return { items, loading, hasMore, sentinelRef };
45}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1An IntersectionObserver lets you trigger loading lazily based on scroll position instead of wiring up scroll listeners.
  2. 2A cancelled flag in an effect cleanup guards against state updates from a stale or out-of-order fetch.
  3. 3A callback ref re-runs whenever the node or its dependencies change, making it ideal for attaching observers to dynamic elements.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Building an infinite scroll hook in React — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code