Dodatne mogućnosti
App router

App router - napredne mogućnosti

Krećemo sa naprednijim mogućnostima "App routera", pri čemu ćemo se fokusirati na tri glavne funkcionalnosti:

  • loading.js - stranica za prikaz indikatora (ili nekog sadržaja) za vrijeme učitavanja stranice
  • not-found.js - za prikaz prilagođenih 404 stranica tj. stranice koju prikazujemo kada traženi resurs/stranica ne postoji
  • nested layout - ugniježđeni raspored, za detaljniju organizaciju sučelja na željenim rutama

Učitavanje stranice

App router arhitektura podržava stvaranje datoteke pod nazivom loading.js unutar željenog direktorija. Ukoliko takva stranica postoji, Next.js će je automatski prikazati kada dođe do izmjene sadržaja ili prilikom učitavanja podataka na toj određenoj ruti.

Npr. ako unutar direktorija pokemon (što odgovara ruti tj. adresi "/pokemon") napravimo ovu stranicu, svi zahtjevi koji se upućuju na adresu /pokemon/_nesto_ će prvo prikazati sadržaj iz loading.js datoteke, dok se svi potrebni podaci na traženoj stranici ne učitaju.

Pogledajmo jednostavni primjer takve stranice:

app/pokemon/loading.js
export default function Loading() {
  return (
    <div style={{ padding: "2rem", textAlign: "center" }}>
      <p>Učitavanje Pokémona...</p>
    </div>
  );
}

U ovom slučaju prikazujemo samo kratku obavijest, ali naravno da možemo napraviti i malo složeniju (i vizualno privlačniju) stranicu za učitavanje - često se koriste tzv. spinner elementi tj. nekakav prikaz animacije kako bi korisnici dobili dojam da se stranica učtiva i da nije blokirana.

💡

React sadrži <Suspense> komponentu koja nam dozvoljava precizniju kontrolu upravljanja stanjem učitavanja - npr. korištenjem ove komponente možemo odmah prikazati fiksne dijelove sučelja, a loading prikaz/animaciju ograničiti samo na dinamički dio sučelja.

Važno je naglasiti da se stranica učitavanja iz primjera odnosi samo na specifične rute (prikaz Pokemona). Umjesto toga smo mogli napraviti i globalnu stranicu za učitavanje - samo je moramo smjestiti direktno u app direktorij.

Nepostojeća stranica

Jedna od češćih pogrešaka u web aplikacijama je kada korisnik pokušava pristupiti nekom resursu koji ne postoji (najčešće se radi o pogrešno upisanom URL-u). Next.js će pozvati funkciju notFound kada ne može pronaći stranicu za odgovarajuću rutu.

Dodavanjem not-found.js datoteke, možemo definirati izgled stranice koja će se prikazati u takvim situacijama. Izgled same stranice ovisi o nama, a uobičajeno je prikazati neku poruku korisniku, sa uputama za provjeru upisane adrese ili povratak na naslovnu stranicu.

Za početak ćemo napraviti globalnu "not-found" stranicu koja će se prikazati u slučaju pogrešno upisane rute:

app/not-found.js
import Link from "next/link";
 
export default function NotFound() {
  return (
    <div>
      <h2>404 - Not Found</h2>
      <p>Tražena stranica na postoji!</p>
      <Link href='/'>Vratite se na početnu stranicu</Link>
    </div>
  );
}

Ukoliko sada u preglednik upišemo URL za koji Next.js ne može pronaći odgovarajuću putanju unutar app direktorija (tj. unutar "app router"-a), prikazati će nam se gornja stranica. U ovom slučaju smo jednostavno ispisali poruku i dodali <Link> komponentu sa vezom na naslovnu stanicu naše aplikacije. Možemo pokušati upisati neku neispravnu putanju - npr. /krivo i vidjeti radi li sve ispravno.

Ipak, i dalje nam ostaje jedan problem - pokušajte ručno upisati adresu /pokemon/kriviPokemon i pogledajte što se događa...

Problem je u tome što je to tehnički ispravna putanja - imamo definiranu dinamičku rutu koja nam dozvoljava da nakon adrese /pokemon/ upišemo dinamičku adresu i nastavak adrese koristimo kao name parametar za dohvat Pokemona sa vanjskog API-ja. Iz konteksta app routera, pronađena je ispravna stranica za prikaz, samo se unutar nje događa neka greška prilikom izvršavanja. Greška koja se javlja je dobivena kao odgovor od vanjskog API-ja. Što znači da prvo moramo u našoj stranici za prikaz Pokemona nadograditi logiku za upravljanje pogreškama (trenutno samo provjeravamo postoji li slika u odgovoru). Trenutno nam ovako izgleda logika slanja zathjeva i obrade odgovora:

app/pokemon/[name]/page.js
export default async function PokemonDetalji({ params }) {
  const { name } = await params;
  const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`);
  const data = await res.json();
  /// ostatak stranice
}

Dodati ćemo osnovnu provjeru je li status odgovora ok i u slučaju da nije, ručno ćemo pozvati notFound metodu koja će uzrokovati prikaz not-found.js stranice:

app/pokemon/[name]/page.js
export default async function PokemonDetalji({ params }) {
  const { name } = await params;
  const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`);
 
  if (!res.ok){
    return notFound()
  }
  const data = await res.json();

Nakon ove promjene, upisivanje krivog imena Pokemona će nam ispravno prikazati (globalnu) not-found stranicu. Možemo ići i korak dalje, napraviti ćemo specifičnu not-found stranicu, samo u slučaju kada se dogodi pogreška pri učitavanju Pokemona. Ime datoteke ostaje isto, samo je moramo smjestiti u odgovarajući direktorij:

app\pokemon\not-found.js
import Link from "next/link";
 
export default function NotFound() {
  return (
    <div className='flex flex-col items-center justify-center min-h-[50vh] p-6 bg-white rounded-md shadow-md text-center mx-auto max-w-xl'>
      <h2 className='text-2xl font-semibold text-gray-800 mb-2'>
        Pokémon ne postoji
      </h2>
      <p className='text-gray-600 mb-4'>
        Nažalost, traženi Pokémon ne postoji. Provjerite jeste li dobro upisali
        adresu.
      </p>
      <Link
        href='/'
        className='text-blue-600 underline hover:text-blue-800 transition-colors'
      >
        ...ili se vratite na početnu stranicu
      </Link>
    </div>
  );
}

Logika stranice je gotovo identična kao i prethodni primjer - koristimo Link za poveznicu na početnu stranicu i ispisujemo korisniku obavijest o pogrešci. Jedina je razlika što smo u ovom primjeru koristili Tailwind za oblikovanje (ukoliko nemate podešen Tailwind na projektu, samo uklonite className atribute).

Nested layout

Još jedna od korisnih funkcionalnosti Next.js-a je tzv. nested layout tj. mogućnost definiranja ugniježđenih rasporeda/izgleda stranice. U prethodnim primjerima smo više puta vidjeli kako neke dijelove aplikacije možemo definirati na razini cijele aplikacije (globalno) ili na razini određene rute (lokalno) - prošli smo primjere globalnog i lokalnog oblikovanja i globalnih i lokalnih not-found stranica. Istu logiku možemo primijeniti i na layout datoteku.

Jedna od prvih stvari na koju smo se osvrnuli kod izrade početnog projekta je bila stranica layout.js unutar app direktorija. U toj stranici smo definirali zadani izgled svih stranica naše aplikacije (dodali smo <header> element). Sada ćemo napraviti i layout koji se odnosi samo na stranice sa detaljima Pokemona. Prateći istu logiku, potrebno je napraviti stranicu layout.js unutar app/pokemon direktorija:

export default function PokemonLayout({ children }) {
  return (
    <div className='flex'>
      <aside className='w-[200px] bg-amber-100'>
        <h2>Pokémon Menu</h2>
      </aside>
      <main className='flex-1 p-4'>{children}</main>
    </div>
  );
}

U ovom primjeru smo jednostavno dodali <aside> oznaku koja se uglavnom koristi za implementaciju navigacijske trake, te smo u <main> element ugnijezdili {children} svojstvo - to nam predstavlja dio stranice unutar kojeg će se prikazivati sadržaj page.js stranice.

Još ćemo malo nadograditi našu aplikaciju kako bi bolje demonstrirali prednost routera i nested layout-a. U izbornik ćemo dodati mogućnost prikazivanja Pokemona prema određenom tipu (zadržati ćemo se na dva tipa zbog jednostavnosti). Prvo što moramo napraviti je definirati strukturu stanica što podrazumijeva stvaranje novih direktorija i datoteka. Unutar pokemon direktorija ćemo stvoriti još dva nova poddirektorija - dragon i ghost - svaki sa svojom page.js datotekom. Struktura bi trebala izgledati ovako:

app/
 └─ pokemon/
     ├─ layout.js
     ├─ loading.js
     ├─ not-found.js
     ├─ [name]/
     │   └─ page.js
     ├─ dragon/       // Nova ruta
     │   └─ page.js
     └─ ghost/        // Nova ruta
         └─ page.js

Za početak je dovoljno napraviti prazne stranice npr:

//app/pokemon/dragon/page.js
export default function DragonPokemon() {
  return <h1>Popis Dragon Pokemona</h1>;
}
//app/pokemon/ghost/page.js
export default function GhostPokemon() {
  return <h1>Popis Ghost Pokemona</h1>;
}

I sada se možemo vratiti na implementaciju bočne trake za navigaciju. Postojeći <aside> element koji nema ništa osim teksta ćemo zamijeniti sa malo složenijim izgledom stranice koji sadrži <nav> element i dva <Link> elementa sa vezom na upravo napravljene stranice (ne zaboravite napraviti import <Link> komponente).

<aside className='w-60 bg-white shadow-lg p-4'>
  <h2 className='text-xl font-bold mb-4'>Pokémon Menu</h2>
  <nav className='space-y-2 mb-4'>
    <ul>
      <li>
        <Link
          href='/pokemon/dragon'
          className='block text-gray-700 hover:text-blue-600'
        >
          Dragon Pokémoni
        </Link>
      </li>
      <li>
        <Link
          href='/pokemon/ghost'
          className='block text-gray-700 hover:text-blue-600'
        >
          Ghost Pokémoni
        </Link>
      </li>
    </ul>
  </nav>
</aside>
💡

Sa ovim primjerom smo izgubili na konzistentnosti adresa - popis svih Pokemona se nalazi na glavnoj stranici (/) dok su popisi određenih tipova na ruti /pokemon/tip. Možda bi bilo bolje da nam se popis svih Pokemona nalazi na ruti /pokemon ali to svakako nije teško za ispraviti - dovoljno nam je dodati page.js datoteku unutar pokemon direktorija

Još ćemo napraviti jednu modifikaciju. Na izborničku traku ćemo također dodati Back tipku kako bi se mogli vraćati na prethodnu stranicu. Prvi problem nam je što nam ova akcija zahtijeva izvršavanje logike na klijentskoj strani (korištenje useRouter hook-a). Mogli bi cijeli izbornik renderirati na klijentskoj strani ali želimo zadržati što više prednosti poslužiteljskog renderiranja pa ćemo samo napraviti izdvojenu <buton> komponentu koja će se icrtavati na klijentskoj strani, te ćemo je uključiti u naš izbornik.

Krenimo od same komponente. Zbog bolje organizacije kôda, napraviti ćemo odvojeni direktorij components u glavnom dijelu aplikacije (izvan app) te unutar njega napraviti jednostavnu komponetnu:

components/BackButton.js
"use client";
 
import { useRouter } from "next/navigation";
 
export default function BackButton() {
  const router = useRouter();
 
  function handleClick() {
    router.back();
  }
 
  return (
    <button
      onClick={handleClick}
      className='bg-gray-200 hover:bg-gray-300 text-gray-800 px-3 py-1 rounded-md transition-colors'
    >
      Back
    </button>
  );
}

Ovdje moramo naglasiti nekoliko detalja:

  • na početku komponente koristimo naredbu "use client" kako bi Next.js-u eksplicitno naglasili da se radi o klijentskoj komponenti.
  • zatim radimo import useRouter hook-a iz next/navigation
  • na početku komponente instanciramo router kako bi mogli pozivati njegove ugrađene metode
  • pritiskom na <button> pozivamo ugrađenu back() metodu koja nas vraća na prethodnu stranicu

Sada imamo komponentu koju možemo uključiti u bilo kojem dijelu aplikacije, kada god nam zatreba tipka za povratak na prethodnu stranicu. U ovom trenutku nam je potrebno samo na jednom mjestu, unutar našeg izbornika (ne zaboravite na import komponente):

<aside className='w-60 bg-white shadow-lg p-4'>
  <h2 className='text-xl font-bold mb-4'>Pokémon Menu</h2>
  <nav className='space-y-2 mb-4'>
    <ul>
        // Link elementi
    </ul>
    <div className="mt-4">
      <BackButton />
    </div>
  </nav>
</aside>
Last updated on April 8, 2025
Digitalna Dalmacija