Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Paginacja i Infinite Scroll

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).

Dwa typy paginacji

1. Paginacja offsetowa (tradycyjna)

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}

2. Paginacja kursorowa

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}

Przycisk "Zaladuj więcej"

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}

Infinite Scroll z IntersectionObserver

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ę.

Ir a CodeWorlds