kodingup.camp

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

Layout, Loading & Error

Modul 5 dari 104 menit bacaLayouts, Loading & Error UI

Isi modul

    Kenapa ini penting

    Aplikasi yang terasa profesional punya tiga hal: kerangka yang konsisten (navbar tidak berkedip saat pindah halaman), umpan balik saat memuat (skeleton, bukan layar putih), dan penanganan error yang ramah (pesan jelas dan tombol "coba lagi", bukan layar crash). Di React biasa kamu harus merakit ketiganya sendiri. Next.js menyediakannya lewat file khusus: layout.js, template.js, loading.js, error.js, dan global-error.js.

    Konsep inti

    Urutan pembungkusan (hierarki komponen)

    Di setiap folder rute, file-file khusus disusun seperti boneka matryoshka (dari luar ke dalam):

    layout.js
    └── template.js
        └── error.js        (error boundary React)
            └── loading.js  (Suspense boundary React)
                └── not-found.js
                    └── page.js atau layout.js milik subfolder
    

    Konsekuensi penting dari urutan ini:

    Layout bersarang

    Layout di folder induk membungkus layout di subfolder. Dengan app/layout.js dan app/blog/layout.js, halaman /blog/halo-nextjs dibungkus root layout lalu blog layout. Layout:

    template.js: seperti layout tapi dibuat ulang

    template.js mirip layout, tapi diberi key unik sehingga dipasang ulang (remount) setiap kali segmennya berganti. Akibatnya state Client Component di dalamnya di-reset, useEffect berjalan lagi, dan elemen DOM dibuat ulang. Gunakan untuk animasi masuk per halaman, atau form yang harus kosong lagi tiap pindah halaman. Perubahan query string saja tidak memicu remount. Kalau ragu, pakai layout; template untuk kebutuhan khusus.

    loading.js dan Suspense

    loading.js otomatis membungkus page.js dengan <Suspense> dari React. Saat halaman menunggu data, user langsung melihat UI loading, lalu konten asli menggantikannya. Analoginya seperti restoran yang langsung menyajikan air putih dan kerupuk begitu kamu duduk, sambil makanan utama dimasak.

    Untuk kontrol lebih halus, pakai <Suspense> langsung di dalam halaman: bagian di luar Suspense tampil seketika, bagian di dalamnya menyusul. Ini disebut streaming. Dokumentasi Next.js menyarankan <Suspense> sedekat mungkin dengan data yang lambat, karena loading.js tidak bisa menutupi data lambat yang diakses di layout pada segmen yang sama.

    error.js: error boundary per rute

    error.js menangkap error tak terduga saat render di segmen tersebut dan menampilkan UI cadangan, sementara layout di atasnya tetap tampil dan bisa dipakai. Aturannya:

    Untuk error yang diharapkan (validasi form gagal, data kosong), jangan throw. Tampilkan pesan biasa atau pakai notFound(). error.js untuk hal yang benar-benar tak terduga.

    global-error.js

    Menangkap error di root layout. Karena ia menggantikan root layout saat aktif, ia wajib punya <html> dan <body> sendiri. Letaknya di app/global-error.js.

    Contoh kode

    Loading untuk semua halaman di /blog:

    // app/blog/loading.jsx
    export default function Loading() {
      return <p className="animate-pulse text-gray-500">Memuat artikel...</p>
    }
    // → saat membuka halaman blog yang datanya belum siap, teks ini tampil dulu
    

    Error boundary untuk /blog:

    // app/blog/error.jsx
    'use client' // error boundary WAJIB Client Component
    
    import { useEffect } from 'react'
    
    export default function ErrorBlog({ error, retry }) {
      useEffect(() => {
        console.error(error) // di proyek nyata: kirim ke layanan monitoring
      }, [error])
    
      return (
        <div className="rounded border border-red-300 bg-red-50 p-4">
          <h2 className="font-bold">Waduh, artikel gagal dimuat.</h2>
          <button onClick={() => retry()} className="mt-2 underline">
            Coba lagi
          </button>
        </div>
      )
    }
    // → navbar dari app/blog/layout.jsx tetap tampil di atas kotak merah ini
    

    Template untuk animasi masuk:

    // app/blog/template.jsx
    export default function BlogTemplate({ children }) {
      // dipasang ulang setiap pindah artikel → animasi berjalan lagi
      return <div className="animate-[fadeIn_0.3s]">{children}</div>
    }
    

    Error global:

    // app/global-error.jsx
    'use client'
    
    export default function GlobalError({ error, retry }) {
      return (
        <html lang="id">
          <body>
            <h2>Aplikasi mengalami masalah serius.</h2>
            <button onClick={() => retry()}>Muat ulang</button>
          </body>
        </html>
      )
    }
    

    Streaming granular dengan beberapa <Suspense>. Bagian yang cepat tidak menunggu bagian yang lambat:

    // app/dashboard/page.jsx
    import { Suspense } from 'react'
    import { connection } from 'next/server'
    
    const tunggu = (ms) => new Promise((r) => setTimeout(r, ms))
    
    async function Ringkasan() {
      await connection() // tandai: bagian ini dirender saat request, bukan saat build
      await tunggu(800)
      return <p>Halo, Sari! Kamu punya 2 tugas.</p>
    }
    
    async function Notifikasi() {
      await connection()
      await tunggu(2000) // sengaja lambat
      return <p>🔔 3 notifikasi baru</p>
    }
    
    export default function DashboardPage() {
      return (
        <main className="space-y-4 p-6">
          <h1 className="text-2xl font-bold">Dashboard</h1> {/* langsung tampil */}
          <Suspense fallback={<p>Memuat ringkasan...</p>}>
            <Ringkasan /> {/* muncul setelah ±0,8 detik */}
          </Suspense>
          <Suspense fallback={<p>Memuat notifikasi...</p>}>
            <Notifikasi /> {/* muncul setelah ±2 detik, tidak menahan Ringkasan */}
          </Suspense>
        </main>
      )
    }
    

    Mencoba error boundary: tambahkan sementara baris ini di app/blog/page.jsx, buka /blog, lalu hapus lagi.

    // app/blog/page.jsx (sementara, untuk uji coba)
    export default async function BlogPage() {
      throw new Error('Database sedang down') // → UI dari error.jsx tampil
    }
    

    Kesalahan umum

    1. Lupa 'use client' di error.js. Next.js menolak karena error boundary harus Client Component.
    2. Mengira error.js menangkap error di layout.js yang sama. Tidak. Pindahkan logika berisiko ke page.js, atau tangani di error.js folder induk/global-error.js.
    3. global-error.js tanpa <html> dan <body>. Wajib, karena ia menggantikan root layout.
    4. Memakai throw untuk error yang wajar. Form tidak valid atau artikel tidak ada bukan "crash". Tampilkan pesan atau pakai notFound().
    5. Satu loading.js besar untuk semuanya. Seluruh halaman jadi menunggu. Lebih baik beberapa <Suspense> di dekat bagian yang lambat.

    Latihan

    1. Mudah, Buat app/produk/loading.jsx berisi tiga kotak abu-abu (skeleton) memakai Tailwind animate-pulse.
    2. Sedang, Buat app/produk/error.jsx yang menampilkan pesan error (kalau error adalah Error, tampilkan error.message) dan tombol "Coba lagi" memanggil retry(). Uji dengan melempar error di page.jsx.
    3. Menantang, Di sebuah halaman, buat dua komponen async: Cuaca (tunggu 1 detik) dan Berita (tunggu 3 detik), masing-masing diawali await connection(). Tampilkan keduanya dengan <Suspense> terpisah, lalu jelaskan urutan tampilnya.
    Lihat solusi

    1.

    // app/produk/loading.jsx
    export default function Loading() {
      return (
        <div className="space-y-3 p-6">
          {[1, 2, 3].map((i) => (
            <div key={i} className="h-16 animate-pulse rounded bg-gray-200" />
          ))}
        </div>
      )
    }
    

    2.

    // app/produk/error.jsx
    'use client'
    
    export default function ErrorProduk({ error, retry }) {
      const pesan = error instanceof Error ? error.message : 'Terjadi kesalahan'
      return (
        <div className="p-6">
          <p className="text-red-600">Gagal memuat produk: {pesan}</p>
          <button onClick={() => retry()} className="mt-2 rounded border px-3 py-1">
            Coba lagi
          </button>
        </div>
      )
    }
    // → di mode dev pesan aslinya tampil; di produksi pesan dari Server Component disamarkan
    

    3.

    // app/info/page.jsx
    import { Suspense } from 'react'
    import { connection } from 'next/server'
    
    const tunggu = (ms) => new Promise((r) => setTimeout(r, ms))
    
    async function Cuaca() {
      await connection()
      await tunggu(1000)
      return <p>☀️ Jakarta 32°C</p>
    }
    
    async function Berita() {
      await connection()
      await tunggu(3000)
      return <p>📰 Harga kopi naik 5%</p>
    }
    
    export default function InfoPage() {
      return (
        <main className="p-6">
          <h1>Info Hari Ini</h1>
          <Suspense fallback={<p>Memuat cuaca...</p>}><Cuaca /></Suspense>
          <Suspense fallback={<p>Memuat berita...</p>}><Berita /></Suspense>
        </main>
      )
    }
    

    Urutannya: judul dan dua teks "Memuat..." tampil seketika, cuaca muncul setelah ±1 detik, berita setelah ±3 detik. Keduanya berjalan bersamaan, jadi total tunggu ±3 detik, bukan 4.

    Ringkasan

    Video pendamping

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

    6. Loading & Error Handling - Next.js 13 Dasar Bahasa IndonesiaFahrezi Adha · Bahasa IndonesiaBuka di YouTube

    Kuis pemahaman

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