Ketika aplikasi web berkembang dari sekadar beberapa halaman menjadi workspace kompleks seperti dashboard admin, inbox, atau aplikasi kolaborasi multi-panel, struktur UI tidak lagi cukup dikelola dengan satu layout global dan beberapa komponen kondisi. Di sinilah App Router pada Next.js menjadi menarik karena memungkinkan layout bertingkat, nested layout, dan parallel routes untuk menyusun antarmuka yang besar tanpa menjadikan pohon komponen sulit dipelihara.
Pada artikel ini, kita akan membahas bagaimana pola tersebut bekerja, kapan sebaiknya digunakan, dan apa konsekuensinya terhadap rendering, loading state, navigasi, serta maintainability. Fokusnya bukan sekadar mengenal folder convention, melainkan memahami alasan arsitektural di balik keputusan desain ini.
Mengapa Layout Bertingkat Penting pada Aplikasi Kompleks
Dalam aplikasi sederhana, satu layout utama sering kali cukup: header, sidebar, lalu konten halaman. Namun pada aplikasi yang lebih besar, kebutuhan UI biasanya bertingkat:
- Layout global untuk tema, session provider, dan navigasi utama.
- Layout area untuk domain tertentu seperti dashboard, settings, atau workspace.
- Layout fitur untuk panel internal seperti daftar percakapan, detail item, inspector, activity feed, dan sebagainya.
Jika semua dikelola lewat satu layout besar plus banyak conditional rendering, beberapa masalah umum akan muncul:
- Komponen layout menjadi terlalu gemuk dan sulit dipahami.
- State UI lokal mudah bocor antar halaman.
- Tanggung jawab data fetching dan loading state bercampur.
- Navigasi antar area menyebabkan rerender yang tidak perlu pada bagian UI yang seharusnya tetap stabil.
Dengan layout bertingkat, setiap level route dapat memiliki layout sendiri yang membungkus children di bawahnya. Artinya, struktur UI bisa disejajarkan dengan struktur URL dan domain aplikasi. Hasilnya biasanya lebih modular dan lebih mudah dirawat.
Dasar Nested Layout di App Router
Di App Router, file layout.tsx pada suatu segmen route akan membungkus semua route di bawah segmen tersebut. Ini memungkinkan kita membangun hierarki layout yang mencerminkan hierarki produk.
Contoh struktur untuk dashboard:
app/
├── layout.tsx
└── dashboard/
├── layout.tsx
├── page.tsx
├── analytics/
│ └── page.tsx
└── users/
├── layout.tsx
├── page.tsx
└── [id]/
└── page.tsxPada struktur di atas:
app/layout.tsxmenangani shell global aplikasi.app/dashboard/layout.tsxmenambahkan sidebar dan navigasi dashboard.app/dashboard/users/layout.tsxdapat menambahkan toolbar atau filter khusus area users.
Contoh implementasi sederhana:
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="dashboard-shell">
<aside className="sidebar">Menu Dashboard</aside>
<main className="content">{children}</main>
</div>
)
}Pola ini berguna karena perubahan route di dalam /dashboard tidak harus membongkar ulang shell dashboard. Secara arsitektural, ini membantu menjaga konsistensi UI dan mengurangi biaya render ulang pada bagian yang stabil.
Kapan Nested Layout Saja Sudah Cukup
Nested layout cocok ketika halaman masih mengikuti pola satu area konten utama yang berganti sesuai route aktif. Contohnya:
- Dashboard dengan sidebar tetap dan satu panel konten utama.
- Area settings dengan navigasi tab di kiri dan detail form di kanan.
- Portal admin dengan struktur modul bertingkat.
Jika kebutuhan UI masih dapat dimodelkan sebagai satu children utama per level route, Anda belum perlu parallel routes.
Parallel Routes: Merender Beberapa Slot UI Sekaligus
Parallel routes memungkinkan satu layout menerima beberapa slot sekaligus, bukan hanya children. Ini sangat berguna untuk antarmuka multi-panel seperti:
- Inbox: daftar percakapan, isi pesan, dan panel detail.
- Workspace: editor utama, panel activity, dan inspector.
- Dashboard operasional: tabel utama, panel filter, dan ringkasan KPI yang hidup berdampingan.
Secara konsep, tiap slot adalah cabang route independen yang dirender paralel di dalam layout yang sama. Ini berbeda dari pola tradisional di mana satu halaman besar memutuskan panel mana yang tampil melalui banyak kondisi lokal.
Contoh struktur folder:
app/
└── workspace/
├── layout.tsx
├── @navigation/
│ └── page.tsx
├── @main/
│ ├── page.tsx
│ └── tasks/
│ └── [taskId]/
│ └── page.tsx
└── @inspector/
├── default.tsx
└── tasks/
└── [taskId]/
└── page.tsxLayout untuk slot tersebut:
// app/workspace/layout.tsx
export default function WorkspaceLayout({
navigation,
main,
inspector,
}: {
navigation: React.ReactNode
main: React.ReactNode
inspector: React.ReactNode
}) {
return (
<div className="workspace-grid">
<nav className="panel-nav">{navigation}</nav>
<section className="panel-main">{main}</section>
<aside className="panel-inspector">{inspector}</aside>
</div>
)
}Dengan struktur ini, Anda dapat merender beberapa bagian UI sekaligus melalui route tree, bukan melalui satu komponen halaman raksasa. Keuntungan utamanya adalah pemisahan tanggung jawab: tiap slot memiliki boundary rendering, data fetching, dan fallback sendiri.
Peran default.tsx
File default.tsx penting untuk slot yang tidak selalu memiliki route aktif. Misalnya panel inspector boleh kosong jika pengguna belum memilih item apa pun.
// app/workspace/@inspector/default.tsx
export default function InspectorDefault() {
return <div className="empty-state">Pilih item untuk melihat detail.</div>
}Tanpa fallback semacam ini, pengalaman pengguna dan perilaku navigasi bisa membingungkan karena slot tidak punya representasi yang jelas saat segmennya belum aktif.
Contoh Kasus Nyata: Inbox Multi-Panel
Inbox adalah contoh klasik untuk parallel routes. Banyak tim awalnya membangun inbox sebagai satu halaman besar dengan state lokal: daftar thread di kiri, isi thread di tengah, detail pelanggan di kanan. Ini terlihat sederhana di awal, tetapi cepat menjadi sulit dipelihara.
Dengan App Router, struktur yang lebih eksplisit bisa seperti ini:
app/
└── inbox/
├── layout.tsx
├── @threads/
│ └── page.tsx
├── @message/
│ ├── default.tsx
│ └── [threadId]/
│ └── page.tsx
└── @details/
├── default.tsx
└── [threadId]/
└── page.tsx// app/inbox/layout.tsx
export default function InboxLayout({
threads,
message,
details,
}: {
threads: React.ReactNode
message: React.ReactNode
details: React.ReactNode
}) {
return (
<div className="inbox-layout">
<aside>{threads}</aside>
<main>{message}</main>
<section>{details}</section>
</div>
)
}Manfaat arsitekturalnya nyata:
- Daftar thread bisa tetap terlihat stabil saat pengguna membuka thread tertentu.
- Panel detail dapat memiliki loading state tersendiri tanpa mengganggu isi pesan.
- Setiap panel lebih mudah diuji dan dikembangkan terpisah.
- URL tetap menjadi sumber kebenaran navigasi, bukan state UI ad-hoc di client.
Parallel Routes vs Conditional Rendering Biasa
Pertanyaan praktisnya: kapan parallel routes memang layak digunakan, dan kapan cukup memakai conditional rendering di dalam satu komponen?
Gunakan Conditional Rendering jika:
- Panel yang berubah hanya bagian kecil dari satu halaman.
- Kontennya ringan dan tidak punya kebutuhan data fetching terpisah.
- State panel sepenuhnya presentasional dan tidak perlu direpresentasikan dalam URL.
- Kompleksitas route tree justru akan lebih besar daripada manfaatnya.
Gunakan Parallel Routes jika:
- Beberapa panel perlu dirender bersamaan dan masing-masing punya lifecycle sendiri.
- Setiap panel memiliki data fetching, loading state, atau error boundary yang berbeda.
- Navigasi antar panel perlu direpresentasikan oleh URL agar dapat dibagikan, di-refresh, atau di-bookmark.
- Aplikasi memiliki struktur multi-panel yang menetap, bukan sekadar modal atau panel sementara.
Trade-off utamanya adalah parallel routes menambah kompleksitas struktur route. Untuk aplikasi kecil, ini bisa terasa berlebihan. Namun untuk produk yang memang multi-panel dan terus berkembang, pendekatan ini sering lebih tahan lama daripada satu halaman besar dengan banyak percabangan logika.
Aturan praktis: jika Anda mulai melihat satu file page atau client component yang mengatur 3-4 panel besar, banyak loading spinner lokal, dan sinkronisasi URL manual, itu tanda kuat bahwa struktur route perlu dinaikkan ke level arsitektur.
Dampak terhadap Loading State dan Error Handling
Salah satu manfaat besar App Router adalah setiap segmen route dapat memiliki loading.tsx dan error.tsx sendiri. Pada parallel routes, ini berarti tiap slot dapat menunjukkan fallback yang berbeda sesuai konteksnya.
Contoh:
app/inbox/@message/[threadId]/loading.tsx
app/inbox/@details/[threadId]/loading.tsxKeuntungannya:
- Membuka thread baru tidak harus membuat seluruh inbox terlihat loading.
- Panel detail pelanggan dapat gagal memuat tanpa menghancurkan panel message.
- Pengalaman pengguna lebih stabil karena hanya bagian yang berubah yang menunjukkan transisi.
Namun ada konsekuensinya: Anda perlu memikirkan fallback per slot secara eksplisit. Kesalahan umum adalah hanya fokus pada layout utama, lalu lupa bahwa slot sekunder juga perlu empty state, loading state, dan error boundary yang layak.
Tips Implementasi
- Pastikan setiap slot penting punya
default.tsxbila tidak selalu aktif. - Gunakan skeleton yang sesuai ukuran panel, bukan spinner generik untuk semuanya.
- Pisahkan kegagalan data per panel agar satu error tidak memblokir seluruh workspace.
Dampak terhadap Navigasi dan Perilaku URL
Parallel routes membuat navigasi menjadi lebih deklaratif karena UI disusun berdasarkan route tree. Ini membawa beberapa konsekuensi penting:
- URL menjadi representasi state UI. Ini baik untuk bookmark, deep-linking, dan restore state saat refresh.
- Navigasi terasa lebih lokal karena hanya slot tertentu yang berubah.
- Transisi lebih terkontrol karena boundary route dipisah dengan jelas.
Namun, tim sering tersandung pada sinkronisasi mental model. Pada pendekatan lama, developer terbiasa berpikir: “klik item, set state selectedId”. Di App Router, cara pikir yang lebih tepat adalah: “klik item, navigasi ke route yang merepresentasikan item tersebut”.
Perubahan ini positif, tetapi membutuhkan disiplin. Jangan mencampur dua sumber kebenaran sekaligus: URL dan state lokal. Jika panel aktif ditentukan oleh route, hindari menduplikasinya dalam state client kecuali benar-benar dibutuhkan untuk interaksi sementara.
Maintainability, Organisasi Folder, dan Batasan Praktis
Struktur route yang baik akan sangat memengaruhi maintainability. Beberapa praktik yang biasanya membantu:
- Kelompokkan berdasarkan domain, bukan semata-mata jenis komponen.
- Jaga layout tetap tipis; layout sebaiknya menyusun shell UI, bukan menampung logika bisnis berat.
- Letakkan data fetching sedekat mungkin dengan slot atau page yang membutuhkannya.
- Hindari slot berlebihan; tidak semua panel layak menjadi parallel route.
Contoh struktur yang masih masuk akal:
app/
└── dashboard/
├── layout.tsx
├── @summary/
│ └── page.tsx
├── @table/
│ └── page.tsx
└── @filters/
├── default.tsx
└── page.tsxTetapi jika Anda mulai memiliki terlalu banyak slot dalam satu layout, misalnya 6-8 panel independen, itu bisa menjadi sinyal bahwa domain UI perlu dipecah lagi atau beberapa panel sebenarnya lebih cocok sebagai komponen biasa.
Kesalahan Umum
- Menggunakan parallel routes untuk elemen kecil yang sebenarnya cukup menjadi komponen.
- Menaruh terlalu banyak logika data di layout, padahal layout idealnya fokus pada komposisi.
- Lupa menyediakan
default.tsxdan fallback untuk slot opsional. - Mencampur state URL dan state lokal untuk panel yang sama.
Debugging Tips
- Periksa kembali nama slot seperti
@mainatau@details; typo kecil sering menyebabkan slot tidak muncul. - Pastikan props di
layout.tsxsesuai dengan nama slot yang didefinisikan. - Gunakan empty state eksplisit untuk membedakan “slot tidak aktif” dengan “data gagal dimuat”.
- Jika navigasi terasa aneh, telusuri kembali apakah state panel dikontrol route atau state client.
Penutup
Layout bertingkat dan parallel routes di Next.js App Router bukan sekadar fitur routing, tetapi alat untuk memodelkan arsitektur UI yang kompleks secara lebih eksplisit. Nested layout membantu mempertahankan shell antarmuka per area aplikasi, sementara parallel routes memungkinkan beberapa panel independen dirender berdampingan dengan lifecycle, loading state, dan navigasi yang lebih terstruktur.
Untuk aplikasi seperti dashboard, inbox, atau workspace multi-panel, pendekatan ini sering lebih unggul daripada conditional rendering biasa karena URL menjadi sumber kebenaran, tanggung jawab panel terpisah dengan jelas, dan maintainability meningkat seiring bertambahnya kompleksitas produk. Meski begitu, parallel routes bukan jawaban untuk semua kasus. Gunakan saat struktur UI memang multi-panel dan memiliki kebutuhan navigasi serta rendering independen yang nyata.
Jika diterapkan dengan disiplin, hasil akhirnya bukan hanya kode yang lebih rapi, tetapi juga antarmuka yang lebih stabil, mudah dikembangkan, dan lebih mudah dipahami oleh tim dalam jangka panjang.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!