javascript 58 lines · 8 steps

Cursor-based infinite scroll with a React hook

A custom hook wraps cursor pagination state and fetching, so a component can render a Load More list without touching the plumbing.

Explained by highlit
1import { useState, useCallback, useEffect } from 'react';
2 
3function usePaginatedProducts(pageSize = 20) {
4 const [items, setItems] = useState([]);
5 const [cursor, setCursor] = useState(null);
6 const [hasMore, setHasMore] = useState(true);
7 const [isLoading, setIsLoading] = useState(false);
8 const [error, setError] = useState(null);
9 
10 const fetchPage = useCallback(async (nextCursor) => {
11 setIsLoading(true);
12 setError(null);
13 try {
14 const params = new URLSearchParams({ limit: String(pageSize) });
15 if (nextCursor) params.set('cursor', nextCursor);
16 const res = await fetch(`/api/products?${params}`);
17 if (!res.ok) throw new Error(`Request failed: ${res.status}`);
18 const { data, nextCursor: newCursor } = await res.json();
19 setItems((prev) => (nextCursor ? [...prev, ...data] : data));
20 setCursor(newCursor);
21 setHasMore(Boolean(newCursor));
22 } catch (err) {
23 setError(err);
24 } finally {
25 setIsLoading(false);
26 }
27 }, [pageSize]);
28 
29 useEffect(() => {
30 fetchPage(null);
31 }, [fetchPage]);
32 
33 const loadMore = useCallback(() => {
34 if (!isLoading && hasMore) fetchPage(cursor);
35 }, [fetchPage, cursor, isLoading, hasMore]);
36 
37 return { items, isLoading, error, hasMore, loadMore };
38}
39 
40export default function ProductList() {
41 const { items, isLoading, error, hasMore, loadMore } = usePaginatedProducts();
42 
43 return (
44 <div className="product-list">
45 <ul>
46 {items.map((product) => (
47 <li key={product.id}>{product.name}</li>
48 ))}
49 </ul>
50 {error && <p role="alert">Failed to load products.</p>}
51 {hasMore && (
52 <button onClick={loadMore} disabled={isLoading}>
53 {isLoading ? 'Loading\u2026' : 'Load More'}
54 </button>
55 )}
56 </div>
57 );
58}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Packaging related state and side effects into a custom hook keeps components focused on rendering.
  2. 2Cursor pagination appends new pages and stops when the server returns no next cursor.
  3. 3Guarding fetches with isLoading and hasMore prevents duplicate or pointless requests.

Related explainers

Share this explainer

Here's the card — post it anywhere.

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