Gdy nasz kosmiczny katalog zawiera tysiace obiektów, nie możemy pobrać ich wszystkich naraz - to by przeciaznylo zarówno serwer, jak i przeglazarke. Rozwiazaniem jest paginacja - dzielenie danych na mniejsze porcje (strony).
Serwer zwraca określona liczbę elementów, poczawszy od konkretnego offsetu:
1// URL: /api/planets?page=1&limit=10
2// Zwraca elementy 1-10
3
4// URL: /api/planets?page=2&limit=10
5// Zwraca elementy 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>Ladowanie strony {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 Poprzednia
44 </button>
45 <span>Strona {page} z {totalPages}</span>
46 <button
47 onClick={() => setPage(p => p + 1)}
48 disabled={page === totalPages}
49 >
50 Nastepna
51 </button>
52 </div>
53 </div>
54 );
55}Zamiast numeru strony, serwer zwraca kursor (wskaznik) do następnej porcji danych. Jest bardziej wydajna dla dużych zbiorow danych:
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 jezeli brak nastepnych
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 ? 'Ładowanie...' : 'Załaduj więcej'}
32 </button>
33 )}
34 </div>
35 );
36}Prostszy wariant - użytkownik klika przycisk, żeby zaladowac następne elementy:
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 nastepnej strony lub 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>Katalog planet ({planets.length})</h2>
32 {planets.map(planet => (
33 <div key={planet.name} className="planet-card">
34 <h3>{planet.name}</h3>
35 <p>Klimat: {planet.climate}</p>
36 </div>
37 ))}
38 {nextUrl && (
39 <button onClick={loadMore} disabled={loading}>
40 {loading ? 'Skanowanie...' : 'Odkryj więcej planet'}
41 </button>
42 )}
43 {!nextUrl && planets.length > 0 && (
44 <p>Przeskanowano cala galaktyke!</p>
45 )}
46 </div>
47 );
48}Automatyczne ładowanie danych gdy użytkownik przewija do konca listy:
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 - uruchomi sie gdy ostatni element
12 // pojawi sie na ekranie
13 const lastItemRef = useCallback(node => {
14 if (loading) return;
15
16 // Odlacz poprzedni 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 // Przypnij ref do ostatniego elementu
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>Ladowanie kolejnych...</p>}
58 {!hasMore && <p>Koniec listy</p>}
59 </div>
60 );
61}IntersectionObserver to natywne API przeglądarki, które pozwala obserwowac, czy element jest widoczny w viewporcie. Gdy ostatni element listy staje sie widoczny, automatycznie ladujemy następna stronę.