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.
- Catch-all
[...slug]menangkap satu segmen atau lebih, hasilnya array./shopsaja tidak cocok. - Optional catch-all
[[...slug]]juga menangkap URL tanpa segmen sama sekali (/docs). Cocok untuk dokumentasi bertingkat. - Nilai params selalu string (atau array string). Kalau butuh angka, ubah sendiri dengan
Number(id).
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' }, ...]
}
- Harus mengembalikan array of object, tiap object berisi semua parameter dinamis rute tersebut.
- Untuk catch-all, nilainya array:
{ slug: ['routing', 'dinamis'] }. - Slug yang tidak ada di daftar tetap bisa diakses: halaman dirender saat pertama kali dikunjungi lalu disimpan untuk pengunjung berikutnya.
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):
generateStaticParamswajib 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.- Tanpa
generateStaticParams, params dianggap data runtime. Akses params harus dibungkus<Suspense>(atau adaloading.js), kalau tidak build gagal. - 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 panggilnotFound()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
- Lupa
await params.params.slugbernilaiundefinedkarenaparamsadalah Promise. Selaluconst { slug } = await params. - Mengira params berupa angka.
/produk/12memberiid: '12'(string). Bandingkan denganNumber(id)atau simpan id sebagai string. generateStaticParamsmengembalikan array kosong saat Cache Components aktif. Build gagal. Kembalikan minimal satu contoh nyata.- Memakai
export const dynamicParams = falsedi proyek Cache Components. Ditolak saat build. GunakannotFound()setelah mengecek data. - Membaca
searchParamsdi layout. Prop itu hanya ada dipage. Di komponen client, pakaiuseSearchParams().
Latihan
- Mudah, Buat
app/kota/[nama]/page.jsxyang menampilkan "Selamat datang di {nama}!". Uji dengan/kota/bandung. - Sedang, Buat
app/menu/[id]/page.jsxdengan data array berisi tiga menu (id,nama,harga). TambahkangenerateStaticParamsdan panggilnotFound()kalau id tidak ada. - Menantang, Buat rute
app/arsip/[tahun]/[bulan]/page.jsxyang menampilkan "Arsip Oktober 2026" untuk/arsip/2026/10. BuatgenerateStaticParamsuntuk bulan 1-12 tahun 2026, dan panggilnotFound()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
[slug]membuat segmen dinamis;[...slug]menangkap banyak segmen;[[...slug]]juga menangkap URL tanpa segmen.paramsdansearchParamsadalah Promise di Next.js 15/16: selaluawait.generateStaticParamsmembuat halaman dinamis menjadi statis saat build.- Dengan Cache Components:
generateStaticParamsminimal satu item,dynamicParamstidak dipakai, dan akses params tanpagenerateStaticParamsharus di dalam<Suspense>. - Gunakan
notFound()darinext/navigationsaat data tidak ditemukan.