We use cookies to enhance your experience on the site
CodeWorlds

Pagination and Infinite Scroll

When our cosmic catalog contains thousands of objects, we can't fetch them all at once - that would overload both the server and the browser. The solution is pagination - dividing data into smaller chunks (pages).

Two Types of Pagination

1. Offset Pagination (Traditional)

The server returns a specific number of elements starting from a specific offset:

1// URL: /api/planets?page=1&limit=10
2// Returns elements 1-10
3
4// URL: /api/planets?page=2&limit=10
5// Returns elements 11-20
6
7function PaginatedList() {
8  const [data, setData] = useState([]);
9  const [page, setPage] = useState(1);
10  const [totalPages, setTotalPages] = useState(1);
11  const [loading, setLoading] = useState(true);
12
13  useEffect(() => {
14    async function fetchPage() {
15      setLoading(true);
16      const response = await fetch(
17        \`https://swapi.dev/api/planets/?page=\${page}\`
18      );
19      const result = await response.json();
20      setData(result.results);
21      setTotalPages(Math.ceil(result.count / 10));
22      setLoading(false);
23    }
24
25    fetchPage();
26  }, [page]);
27
28  return (
29    <div>
30      {loading ? (
31        <p>Loading page {page}...</p>
32      ) : (
33        data.map(planet => (
34          <div key={planet.name}>{planet.name}</div>
35        ))
36      )}
37
38      <div className="pagination">
39        <button
40          onClick={() => setPage(p => p - 1)}
41          disabled={page === 1}
42        >
43          Previous
44        </button>
45        <span>Page {page} of {totalPages}</span>
46        <button
47          onClick={() => setPage(p => p + 1)}
48          disabled={page === totalPages}
49        >
50          Next
51        </button>
52      </div>
53    </div>
54  );
55}

2. Cursor Pagination

Instead of a page number, the server returns a cursor (pointer) to the next chunk of data. It's more efficient for large datasets:

1function CursorPaginatedList() {
2  const [items, setItems] = useState([]);
3  const [nextCursor, setNextCursor] = useState(null);
4  const [loading, setLoading] = useState(false);
5
6  const loadMore = async () => {
7    setLoading(true);
8    const url = nextCursor
9      ? \`/api/stars?cursor=\${nextCursor}&limit=20\`
10      : '/api/stars?limit=20';
11
12    const response = await fetch(url);
13    const data = await response.json();
14
15    setItems(prev => [...prev, ...data.items]);
16    setNextCursor(data.nextCursor); // null if no more items
17    setLoading(false);
18  };
19
20  useEffect(() => {
21    loadMore();
22  }, []);
23
24  return (
25    <div>
26      {items.map(item => (
27        <div key={item.id}>{item.name}</div>
28      ))}
29      {nextCursor && (
30        <button onClick={loadMore} disabled={loading}>
31          {loading ? 'Loading...' : 'Load more'}
32        </button>
33      )}
34    </div>
35  );
36}

"Load More" Button

A simpler variant - the user clicks a button to load the next batch of elements:

1function LoadMoreList() {
2  const [planets, setPlanets] = useState([]);
3  const [nextUrl, setNextUrl] = useState(
4    'https://swapi.dev/api/planets/'
5  );
6  const [loading, setLoading] = useState(false);
7
8  const loadMore = async () => {
9    if (!nextUrl || loading) return;
10
11    setLoading(true);
12    try {
13      const response = await fetch(nextUrl);
14      const data = await response.json();
15
16      setPlanets(prev => [...prev, ...data.results]);
17      setNextUrl(data.next); // URL of next page or null
18    } catch (err) {
19      console.error(err);
20    } finally {
21      setLoading(false);
22    }
23  };
24
25  useEffect(() => {
26    loadMore();
27  }, []);
28
29  return (
30    <div>
31      <h2>Planet Catalog ({planets.length})</h2>
32      {planets.map(planet => (
33        <div key={planet.name} className="planet-card">
34          <h3>{planet.name}</h3>
35          <p>Climate: {planet.climate}</p>
36        </div>
37      ))}
38      {nextUrl && (
39        <button onClick={loadMore} disabled={loading}>
40          {loading ? 'Scanning...' : 'Discover more planets'}
41        </button>
42      )}
43      {!nextUrl && planets.length > 0 && (
44        <p>The entire galaxy has been scanned!</p>
45      )}
46    </div>
47  );
48}

Infinite Scroll with IntersectionObserver

Automatically loading data when the user scrolls to the end of the list:

1import { useRef, useCallback } from 'react';
2
3function InfiniteScrollList() {
4  const [items, setItems] = useState([]);
5  const [page, setPage] = useState(1);
6  const [hasMore, setHasMore] = useState(true);
7  const [loading, setLoading] = useState(false);
8
9  const observer = useRef();
10
11  // Ref callback - fires when the last element
12  // appears on screen
13  const lastItemRef = useCallback(node => {
14    if (loading) return;
15
16    // Disconnect previous observer
17    if (observer.current) observer.current.disconnect();
18
19    observer.current = new IntersectionObserver(entries => {
20      if (entries[0].isIntersecting && hasMore) {
21        setPage(prev => prev + 1);
22      }
23    });
24
25    if (node) observer.current.observe(node);
26  }, [loading, hasMore]);
27
28  useEffect(() => {
29    async function loadPage() {
30      setLoading(true);
31      const response = await fetch(
32        \`https://swapi.dev/api/people/?page=\${page}\`
33      );
34      const data = await response.json();
35
36      setItems(prev => [...prev, ...data.results]);
37      setHasMore(data.next !== null);
38      setLoading(false);
39    }
40
41    loadPage();
42  }, [page]);
43
44  return (
45    <div>
46      {items.map((item, index) => {
47        // Attach ref to the last element
48        if (index === items.length - 1) {
49          return (
50            <div ref={lastItemRef} key={item.name}>
51              {item.name}
52            </div>
53          );
54        }
55        return <div key={item.name}>{item.name}</div>;
56      })}
57      {loading && <p>Loading more...</p>}
58      {!hasMore && <p>End of list</p>}
59    </div>
60  );
61}

IntersectionObserver is a native browser API that lets you observe whether an element is visible in the viewport. When the last item in the list becomes visible, we automatically load the next page.

Go to CodeWorlds