kodingup.camp

Beranda/Next.js Mastery: Dasar, Routing & Rendering

Route Dinamis

Modul 4 dari 104 menit bacaDynamic Routes

Isi modul

    Kenapa ini penting

    Bayangkan toko online dengan 5.000 produk. Kamu tentu tidak membuat 5.000 folder. Yang kamu butuhkan adalah satu file halaman yang bisa menampilkan produk apa pun berdasarkan URL: /produk/kopi-gayo, /produk/teh-tarik, dan seterusnya. Itulah route dinamis. Hampir semua aplikasi nyata memakainya: artikel blog, profil user, detail pesanan, halaman kategori.

    Di Next.js 16 ada satu perubahan yang sering membuat kode lama error: params sekarang berupa Promise yang harus di-await. Modul ini membahasnya sampai tuntas.

    Konsep inti

    Segmen dinamis [slug]

    Bungkus nama folder dengan kurung siku. Nama di dalam kurung menjadi nama parameter.

    File Contoh URL Nilai params
    app/blog/[slug]/page.js /blog/halo-nextjs { slug: 'halo-nextjs' }
    app/toko/[kategori]/[id]/page.js /toko/kopi/12 { kategori: 'kopi', id: '12' }
    app/shop/[...slug]/page.js /shop/baju/kaos { slug: ['baju', 'kaos'] }
    app/docs/[[...slug]]/page.js /docs atau /docs/a/b { slug: undefined } atau { slug: ['a', 'b'] }

    Analoginya seperti formulir dengan kolom kosong: "Halo, ____". Satu formulir dipakai untuk semua orang; isinya ditentukan saat diisi. Slug adalah teks ramah URL yang mewakili sebuah data, biasanya huruf kecil dengan tanda hubung.

    params adalah Promise

    Di Next.js 15 ke atas, params (dan searchParams) yang diterima page, layout, dan generateMetadata berupa Promise:

    // BENAR (Next.js 15/16)
    export default async function Page({ params }) {
      const { slug } = await params
    }
    
    // SALAH (gaya Next.js 14 ke bawah)
    export default function Page({ params }) {
      const slug = params.slug // → undefined, karena params adalah Promise
    }
    

    Kenapa Promise? Supaya Next.js bisa mulai merender bagian halaman yang tidak butuh params sebelum params tersedia. Ini kunci Partial Prerendering.

    Kalau kamu memakai TypeScript, Next.js membuat tipe global PageProps<'/blog/[slug]'> dan LayoutProps<'/blog'> otomatis saat next dev/next build, jadi kamu tidak perlu menulis tipe params manual.

    searchParams untuk query string

    URL /produk?q=kopi&hal=2 punya query string. Di page, baca lewat prop searchParams (juga Promise): const { q } = await searchParams. Di Client Component, pakai hook useSearchParams() dari next/navigation. searchParams hanya ada di page, tidak di layout.

    generateStaticParams: buat halaman saat build

    Secara default Next.js tidak tahu slug apa saja yang ada. Fungsi generateStaticParams memberi tahu daftarnya, sehingga halaman-halaman itu dibuat saat build (statis, super cepat):

    export async function generateStaticParams() {
      const posts = await getPosts()
      return posts.map((post) => ({ slug: post.slug })) // → [{ slug: 'halo-nextjs' }, ...]
    }
    

    Aturan khusus saat Cache Components aktif

    Proyek baru Next.js 16.4 mengaktifkan cacheComponents. Ini menambah beberapa aturan (semuanya kami uji dengan npm run build):

    1. generateStaticParams wajib mengembalikan minimal satu item. Array kosong membuat build gagal: "When using Cache Components, all generateStaticParams functions must return at least one result." Tujuannya agar Next.js bisa memvalidasi halaman dengan contoh nyata.
    2. Tanpa generateStaticParams, params dianggap data runtime. Akses params harus dibungkus <Suspense> (atau ada loading.js), kalau tidak build gagal.
    3. Opsi lama export const dynamicParams = false (untuk membuat slug di luar daftar otomatis 404) tidak kompatibel dengan Cache Components; build menolak dengan pesan "Route segment config dynamicParams is not compatible with nextConfig.cacheComponents". Gantinya: cek datanya, lalu panggil notFound() kalau tidak ada.

    notFound() untuk data yang tidak ada

    import { notFound } from 'next/navigation'. Saat dipanggil, Next.js berhenti merender halaman dan menampilkan not-found.js terdekat. Kamu bisa membuat app/blog/[slug]/not-found.js khusus untuk artikel.

    Contoh kode

    Data tiruan (di proyek nyata ini query database). Bagian 'use cache' akan dijelaskan di modul Caching; untuk sekarang anggap saja "hasilnya disimpan agar cepat".

    // lib/posts.js
    import { cacheLife, cacheTag } from 'next/cache'
    
    const POSTS = [
      { slug: 'halo-nextjs', judul: 'Halo, Next.js!', isi: 'Artikel pertama kita.' },
      { slug: 'routing-itu-folder', judul: 'Routing Itu Folder', isi: 'Folder = URL.' },
      { slug: 'server-component', judul: 'Kenalan dengan Server Component', isi: 'Render di server.' },
    ]
    
    const tunggu = (ms) => new Promise((r) => setTimeout(r, ms)) // simulasi database lambat
    
    export async function getPosts() {
      'use cache'
      cacheLife('hours')
      cacheTag('posts')
      await tunggu(300)
      return POSTS
    }
    
    export async function getPost(slug) {
      'use cache'
      cacheLife('hours')
      cacheTag('posts', `post-${slug}`)
      await tunggu(300)
      return POSTS.find((p) => p.slug === slug) ?? null
    }
    

    Halaman detail artikel:

    // app/blog/[slug]/page.jsx
    import { notFound } from 'next/navigation'
    import { getPost, getPosts } from '@/lib/posts'
    
    // Daftar slug yang dibuat saat build
    export async function generateStaticParams() {
      const posts = await getPosts()
      return posts.map((post) => ({ slug: post.slug }))
    }
    
    export default async function PostPage({ params }) {
      const { slug } = await params // ← WAJIB await
      const post = await getPost(slug)
    
      if (!post) notFound() // → tampilkan not-found.jsx terdekat
    
      return (
        <article className="space-y-4">
          <h1 className="text-3xl font-bold">{post.judul}</h1>
          <p>{post.isi}</p>
        </article>
      )
    }
    
    // app/blog/[slug]/not-found.jsx
    import Link from 'next/link'
    
    export default function ArtikelTidakAda() {
      return (
        <div>
          <h2 className="text-xl font-bold">Artikel tidak ditemukan</h2>
          <Link href="/blog" className="underline">Lihat semua artikel</Link>
        </div>
      )
    }
    

    Hasil npm run build di proyek uji:

    npm run build
    # → ├   /blog/[slug]
    # → │ ├ ◐ /blog/[slug]
    # → │ ├ ○ /blog/halo-nextjs           1h  1d
    # → │ ├ ○ /blog/routing-itu-folder    1h  1d
    # → │ └ ○ /blog/server-component      1h  1d
    # → ○ = statis, ◐ = Partial Prerender (kerangka statis + bagian dinamis)
    

    Optional catch-all untuk dokumentasi:

    // app/docs/[[...slug]]/page.jsx
    export function generateStaticParams() {
      return [{ slug: [] }, { slug: ['instalasi'] }, { slug: ['routing', 'dinamis'] }]
    }
    
    export default async function DocsPage({ params }) {
      const { slug = [] } = await params // /docs → slug undefined, jadi beri default []
      return <p>Bagian dokumentasi: {slug.length ? slug.join(' / ') : 'beranda'}</p>
    }
    // → /docs                  → "Bagian dokumentasi: beranda"
    // → /docs/routing/dinamis  → "Bagian dokumentasi: routing / dinamis"
    

    Pencarian dengan searchParams. Karena query string baru diketahui saat request, bagian yang membacanya dibungkus <Suspense> sehingga judul dan form tetap bisa dibuat statis:

    // app/produk/page.jsx
    import { Suspense } from 'react'
    
    const PRODUK = ['Kopi Gayo', 'Teh Tarik', 'Kopi Toraja', 'Cokelat Bali']
    
    async function Hasil({ searchParams }) {
      const { q = '' } = await searchParams
      const hasil = PRODUK.filter((p) => p.toLowerCase().includes(q.toLowerCase()))
      return <ul>{hasil.map((p) => <li key={p}>{p}</li>)}</ul>
    }
    
    export default function ProdukPage({ searchParams }) {
      return (
        <main className="p-6">
          <h1 className="text-2xl font-bold">Produk</h1>
          <form>
            <input name="q" placeholder="Cari..." className="border px-2" />
          </form>
          <Suspense fallback={<p>Mencari...</p>}>
            <Hasil searchParams={searchParams} /> {/* Promise diteruskan, di-await di dalam */}
          </Suspense>
        </main>
      )
    }
    // → /produk?q=kopi → Kopi Gayo, Kopi Toraja
    

    Kesalahan umum

    1. Lupa await params. params.slug bernilai undefined karena params adalah Promise. Selalu const { slug } = await params.
    2. Mengira params berupa angka. /produk/12 memberi id: '12' (string). Bandingkan dengan Number(id) atau simpan id sebagai string.
    3. generateStaticParams mengembalikan array kosong saat Cache Components aktif. Build gagal. Kembalikan minimal satu contoh nyata.
    4. Memakai export const dynamicParams = false di proyek Cache Components. Ditolak saat build. Gunakan notFound() setelah mengecek data.
    5. Membaca searchParams di layout. Prop itu hanya ada di page. Di komponen client, pakai useSearchParams().

    Latihan

    1. Mudah, Buat app/kota/[nama]/page.jsx yang menampilkan "Selamat datang di {nama}!". Uji dengan /kota/bandung.
    2. Sedang, Buat app/menu/[id]/page.jsx dengan data array berisi tiga menu (id, nama, harga). Tambahkan generateStaticParams dan panggil notFound() kalau id tidak ada.
    3. Menantang, Buat rute app/arsip/[tahun]/[bulan]/page.jsx yang menampilkan "Arsip Oktober 2026" untuk /arsip/2026/10. Buat generateStaticParams untuk bulan 1-12 tahun 2026, dan panggil notFound() kalau bulan di luar 1-12.
    Lihat solusi

    1.

    // app/kota/[nama]/page.jsx
    export function generateStaticParams() {
      return [{ nama: 'bandung' }, { nama: 'medan' }] // minimal satu item (Cache Components)
    }
    
    export default async function KotaPage({ params }) {
      const { nama } = await params
      return <h1 className="text-2xl">Selamat datang di {nama}!</h1>
    }
    // → /kota/bandung → "Selamat datang di bandung!"
    

    2.

    // app/menu/[id]/page.jsx
    import { notFound } from 'next/navigation'
    
    const MENU = [
      { id: '1', nama: 'Kopi Susu', harga: 18000 },
      { id: '2', nama: 'Americano', harga: 20000 },
      { id: '3', nama: 'Teh Tarik', harga: 15000 },
    ]
    
    export function generateStaticParams() {
      return MENU.map((m) => ({ id: m.id }))
    }
    
    export default async function MenuPage({ params }) {
      const { id } = await params
      const menu = MENU.find((m) => m.id === id)
      if (!menu) notFound()
    
      return (
        <div>
          <h1 className="text-2xl font-bold">{menu.nama}</h1>
          <p>Rp{menu.harga.toLocaleString('id-ID')}</p>
        </div>
      )
    }
    // → /menu/2 → Americano, Rp20.000
    // → /menu/99 → halaman not-found
    

    3.

    // app/arsip/[tahun]/[bulan]/page.jsx
    import { notFound } from 'next/navigation'
    
    const NAMA_BULAN = ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
      'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember']
    
    export function generateStaticParams() {
      return NAMA_BULAN.map((_, i) => ({ tahun: '2026', bulan: String(i + 1) }))
    }
    
    export default async function ArsipPage({ params }) {
      const { tahun, bulan } = await params
      const nomor = Number(bulan)
      if (!Number.isInteger(nomor) || nomor < 1 || nomor > 12) notFound()
    
      return <h1 className="text-2xl">Arsip {NAMA_BULAN[nomor - 1]} {tahun}</h1>
    }
    // → /arsip/2026/10 → "Arsip Oktober 2026"
    // → /arsip/2026/13 → not-found
    

    Ringkasan

    Video pendamping

    Kalau penjelasan di atas belum masuk, tonton ini. Video dari kanal YouTube pihak lain.

    Next.js 15 Tutorial - 7 - Dynamic RoutesCodevolution · Bahasa InggrisBuka di YouTube

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.