Dodatne mogućnosti
Paginacija

Paginacija

Sada ćemo dodati još nekoliko osnovnih funkcionalnosti koje bi trebala imati aplikacija ovog tipa. Već smo spominjali kako nije praktično na jednom ekranu prikazivati sve Pokemone pa ćemo implementirati mogućnost prikaza na više stranica (paginacija).

Prvo se moramo kratko prebaciti na dokumentaciju našeg PokeAPI (opens in a new tab)-ja gdje možemo vidjeti da sam API podržava paginaciju uz pomoć limit i offset parametara pomoću kojih možemo prilikom slanja zahtjeva definirati koliko rezultata želimo dobiti (limit) i od kojeg zapisa želimo započeti (offset - pomak od početka).

Npr. do sada smo na početnoj stranici dohvaćali prvih 20 Pokemona, kada bi htjeli dohvatiti idućih 20, adresa za slanje GET zahtjeva bi trebala izgledati ovako:

"https://pokeapi.co/api/v2/ability/?limit=20&offset=20"
💡

Moguće je da neki API ne budu imali ugrađenu mogućnost paginacije, tada je implementacija malo složenija ali svejedno se može napraviti - npr. spremiti rezultate u lokalni spremnik ili state pa prikazivati samo dio podataka.

Sada kada znamo na koju adresu slati zahtjeve, napraviti ćemo još jednu dinamičku rutu kako bi mogli dohvaćati Pokemone po stranicama. Slično kao što smo omogućili dohvat Pokemona po njegovom imenu, omogućiti ćemo slanje broja stranice kao parametra, i ovisno o tom broju dohvaćati različite nizove od 20 Pokemona. Zamišljena struktura izgleda ovako (moramo stvoriti direktorije i datoteke):

app/
 └─ pokemon/
     └─ page/
         └─ [pageNumber]/
             └─ page.js

Kada smo napravili strukturu, započeti ćemo (i testirati) sa logikom dohvata Pokemona prema dobivenom parametru iz adrese. Sve što moramo je dohvatiti parametar pageNumber iz rute te ga iskoristi kako bi izračunali vrijednost offset parametra (limit ćemo držati konstantno na broju 20):

app/pokemon/page/[pageNumber]/page.js
export default async function PokemonPage({ params }) {
  const { pageNumber } = await params;
  const brStranice = parseInt(pageNumber, 10) || 1; // ako je krivi parametar
  const limit = 20;
  const offset = (brStranice - 1) * limit;
 
  const res = await fetch(
    `https://pokeapi.co/api/v2/pokemon?offset=${offset}&limit=${limit}`
  );
  const data = await res.json();
 
  console.log(data);
 
  return <div></div>;
}

Logika je zapravo jednostavna, s obzirom da uvijek učitavamo 20 Pokemona, samo moramo broj stranice (umanjen za 1) pomnožiti sa 20 (limit) kako bi dobili offset parametar - tj. kako bi izračunali koliko Pokemona želimo "prekočiti". Također smo dodali jednostavnu "zaštitu" od upisivanja krivog broja - ako se ne može pretvoriti u integer onda ga postavljamo na 1.

Možemo u preglednik upisati adresu - npr. /pokemon/page/3 i trebali bi u konzoli vidjeti ispis Pokemona od brojeva 41 do 60.

Nastaviti ćemo sa implementacijom prikaza - tu imamo dva zadatka: prikazati imena dohvaćenih Pokemona i omogućiti učitavanje prethodne i iduće stranice. Krenimo za prikazom:

app/pokemon/page/[pageNumber]/page.js
return (
  <main className='p-4'>
    <h1 className='text-2xl mb-4'>Pokémon – Stranica {brStranice}</h1>
    <ul className='space-y-1'>
      {data.results.map(p => (
        <li key={p.name}>{p.name}</li>
      ))}
    </ul>
  </main>
)

Prikaz nam je gotovo identičan kao i na prvoj stranici koju smo implementirali na prethodnoj lekciji. Jedina je razlika što ovaj put direktno prikazujemo Pokemone iz rezultata zahtjeva umjesto da ih spremamo u odvojenu varijablu. Za sad prikazujemo samo imena - bez opcije otvaranja detalja, to možemo naknadno dodati. Prvo ćemo se fokusirati na drugi dio problema - implementirati naprijed/nazad kontrole za "listanje" stranica.

Glavni dio posla smo već napravili - imamo dinamičke rute. Sve što nam je preostalo je dodati <Link> elemente koji vode na stranicu prije i nakon naše trenutne stanice. Također ćemo iskoristiti uvjetno renderiranje kako se na prvoj stranici ne bi pokazivala opcija za prethodnu stranicu. Kompletni izgled stranice bi trebao izgledati otprilike ovako:

app/pokemon/page/[pageNumber]/page.js
import Link from "next/link";
 
export default async function PokemonPage({ params }) {
  const { pageNumber } = await params;
  const brStranice = parseInt(pageNumber, 10) || 1; // ako je krivi parametar
  const limit = 20;
  const offset = (brStranice - 1) * limit;
 
  const res = await fetch(
    `https://pokeapi.co/api/v2/pokemon?offset=${offset}&limit=${limit}`
  );
  const data = await res.json();
 
  return (
    <main className='p-4'>
      <h1 className='text-2xl mb-4'>Pokémon - Stranica {brStranice}</h1>
      <ul className='space-y-1'>
        {data.results.map(p => (
          <li key={p.name}>{p.name}</li>
        ))}
      </ul>
 
      <div className='mt-4 flex gap-2'>
        {brStranice > 1 && (
          <Link
            href={`/pokemon/page/${brStranice - 1}`}
            className='px-2 py-1 bg-gray-300'
          >
            Prethodna
          </Link>
        )}
        <Link
          href={`/pokemon/page/${brStranice + 1}`}
          className='px-2 py-1 bg-gray-300'
        >
          Iduća
        </Link>
      </div>
    </main>
  );
}

Poslužiteljske stranice - pregled

Gotovo svi prethodni primjeri su uključivali stranice koje se iscrtavaju (renderiraju) na poslužitelju. Na poslužiteljskim stranicama ne možemo koristiti React hook-ove ali možemo unaprijed dohvaćati podatke sa fetch metodom. Postavljanjem određenih opcija, možemo upravljati na koji način se odvija poslužiteljsko iscrtavanje stranice, imamo tri ključna pojma:

  1. SSG (Static Site Generation): Stranica se generira jednom tijekom builda ili prilikom prvog HTML zahtjeva (ovisno o postavkama). Pri svakom sljedećem zahtjevu za tom stranicom, poslužuje se već generirani HTML.
  2. SSR (Server-Side Rendering): Stranica se renderira na poslužitelju pri svakom upitu. Svaki puta kad korisnik pošalje zahtjev, fetch se ponovno poziva i poslužitelj generira "svježi" HTML.
  3. ISR (Incremental Static Regeneration): Stranica se generira u statičkom obliku, ali se povremeno (npr. svakih n sekundi) može revalidirati, pa se generira nova verzija koja zamjenjuje staru.

Primjeri

Static Site Generation - uvijek koristimo predmemoriranu (cache-iranu) verziju stranice

  const res = await fetch('https://pokeapi.co/api/v2/pokemon?limit=20', {
    cache: 'force-cache', // generira i čuva HTML
  });

Server Side Rendering - svaki puta se poziva novi fetch:

  const res = await fetch('https://pokeapi.co/api/v2/pokemon?limit=20', {
    cache: 'no-store', // svaki puta svježi dohvat
  });

Incremental Static Regeneration - definiramo "trajanje" stranice:

export const revalidate = 60; 
 
export default async function PokemonLista() {
  const res = await fetch('https://pokeapi.co/api/v2/pokemon?limit=20', {
    // Možemo ostaviti "cache: 'force-cache'"
  });
  // Ostatak komponente
}

Prethodni primjeri su opcionalni. Ako smo sve prije toga dobro napravili, stranica nam ima potpunu funkcionalnost učitavanja Pokemona po stranicama. Vrijeme da se malo prebacimo i na klijentsku stranu aplikacije.

Last updated on April 15, 2025
Digitalna Dalmacija