kodingup.camp

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

Data Fetching di Server

Modul 7 dari 105 menit bacaData Fetching

Isi modul

    Kenapa ini penting

    Di materi TanStack Query kamu mengambil data dari browser: komponen tampil, lalu request dikirim, lalu loading, lalu data muncul. Di Next.js ada cara yang lebih langsung: ambil data di server, sebelum HTML dikirim. Tidak ada endpoint API perantara, tidak ada useEffect, tidak ada state loading manual, dan API key tidak pernah sampai ke browser. Tapi kamu juga harus tahu jebakannya: request berurutan yang tidak perlu (waterfall) bisa membuat halaman lambat. Modul ini membahas cara benar mengambil data, menjalankannya paralel, dan men-stream hasilnya.

    Konsep inti

    Server Component boleh async

    Karena berjalan di server, Server Component boleh berupa async function dan memakai await langsung di dalamnya. Ini tidak bisa dilakukan di Client Component.

    export default async function Page() {
      const res = await fetch('https://api.contoh.id/produk')
      const produk = await res.json()
      return <ul>{produk.map((p) => <li key={p.id}>{p.nama}</li>)}</ul>
    }
    

    Hal yang perlu diingat tentang fetch di server Next.js 16:

    Query database langsung

    Server Component bisa langsung memakai ORM atau driver database (Prisma, Drizzle, pg). Kode query dan kredensialnya tidak ikut ke bundle client. Kamu tetap wajib memeriksa siapa user-nya (autentikasi dan otorisasi) sebelum mengembalikan data sensitif.

    Untuk fungsi non-fetch (misalnya query ORM) yang dipanggil beberapa komponen dalam satu request, bungkus dengan cache dari React agar hanya dijalankan sekali per request.

    Berurutan (sequential) vs paralel

    Analogi: kamu harus beli kopi, roti, dan koran. Berurutan berarti antre kopi sampai selesai, baru antre roti, baru antre koran. Paralel berarti mengutus tiga teman sekaligus. Total waktunya cuma selama antrean terlama.

    // BERURUTAN, total ≈ 500 + 800 = 1300 ms
    const user = await getUser()   // 500 ms
    const tugas = await getTugas() // 800 ms, baru mulai setelah getUser selesai
    
    // PARALEL, total ≈ 800 ms
    const [user, tugas] = await Promise.all([getUser(), getTugas()])
    

    Berurutan hanya wajar kalau request kedua butuh hasil request pertama (misalnya ambil artis dulu, lalu playlist berdasarkan artis.id). Kalau tidak saling bergantung, jalankan paralel. Ingat: kalau salah satu promise di Promise.all gagal, semuanya dianggap gagal; pakai Promise.allSettled kalau ingin tetap menampilkan yang berhasil.

    Streaming dengan Suspense

    Promise.all mempercepat, tapi halaman tetap menunggu request terlama. Dengan <Suspense> setiap bagian bisa tampil begitu datanya siap. Bagian lambat tidak menahan bagian cepat. Ini yang membuat dashboard terasa responsif.

    Saat Cache Components aktif (default proyek baru), aturannya tegas: data yang tidak di-cache, cookies(), headers(), searchParams, dan connection() harus berada di dalam <Suspense> atau di-cache dengan "use cache". Kalau tidak, build gagal dengan pesan "Next.js encountered uncached or runtime data during prerendering" beserta tiga pilihan perbaikan: bungkus dengan Suspense, cache dengan "use cache", atau tandai rute sebagai blocking dengan export const instant = false. Aturan ini memaksa setiap halaman punya "kerangka statis" yang bisa langsung dikirim.

    connection(): tandai "harus saat request"

    Kadang komponen tidak membaca cookie atau fetch apa pun, tapi tetap harus segar per request, misalnya menampilkan jam server atau Math.random(). Panggil await connection() (dari next/server) di awal komponen itu dan bungkus dengan <Suspense>. Tanpa ini, Next.js tidak tahu apakah nilai itu boleh dibekukan saat build.

    Meneruskan Promise ke Client Component dengan use

    Kamu bisa memulai fetch di Server Component tanpa await, lalu mengoper Promise-nya ke Client Component, dan membacanya dengan hook use() dari React di dalam <Suspense>. Server tidak menunggu, dan client menerima data begitu siap.

    Kapan masih pakai TanStack Query?

    Data awal halaman: ambil di server. TanStack Query (atau SWR) tetap berguna untuk data yang berubah karena interaksi user di client: infinite scroll, polling, filter yang berganti cepat, atau optimistic update.

    Contoh kode

    Fetch dari API publik di Server Component:

    // app/pengguna/page.jsx
    import { Suspense } from 'react'
    
    async function DaftarPengguna() {
      const res = await fetch('https://jsonplaceholder.typicode.com/users?_limit=3')
      if (!res.ok) throw new Error('Gagal memuat pengguna') // → ditangkap error.js
      const users = await res.json()
      return (
        <ul className="list-disc pl-6">
          {users.map((u) => <li key={u.id}>{u.name} - {u.email}</li>)}
        </ul>
      )
    }
    
    export default function PenggunaPage() {
      return (
        <main className="p-6">
          <h1 className="text-2xl font-bold">Pengguna</h1>
          <Suspense fallback={<p>Memuat pengguna...</p>}>
            <DaftarPengguna /> {/* fetch tanpa cache → wajib di dalam Suspense */}
          </Suspense>
        </main>
      )
    }
    // → Leanne Graham, Sincere@april.biz, dan seterusnya
    

    Query database dengan React.cache (contoh memakai Prisma; struktur sama untuk ORM lain):

    // lib/user.js
    import 'server-only'
    import { cache } from 'react'
    import { prisma } from '@/lib/prisma'
    
    // Dipanggil di layout DAN page dalam satu request? Query tetap jalan sekali.
    export const getUser = cache(async (id) => {
      return prisma.user.findUnique({ where: { id } })
    })
    

    Paralel plus streaming di satu halaman:

    // app/dashboard/page.jsx
    import { Suspense } from 'react'
    import { connection } from 'next/server'
    
    const tunggu = (ms) => new Promise((r) => setTimeout(r, ms))
    
    async function getUser() { await tunggu(500); return { nama: 'Sari' } }
    async function getTugas() { await tunggu(800); return ['Belajar routing', 'Belajar caching'] }
    async function getNotif() { await tunggu(2000); return 3 }
    
    async function Ringkasan() {
      await connection() // data per request, jangan dibekukan saat build
      const [user, tugas] = await Promise.all([getUser(), getTugas()]) // paralel ≈ 800 ms
      return (
        <div>
          <p>Halo, {user.nama}! Kamu punya {tugas.length} tugas.</p>
          <p>Jam server: {new Date().toLocaleTimeString('id-ID')}</p>
        </div>
      )
    }
    
    async function Notifikasi() {
      const jumlah = await getNotif() // 2 detik, tapi tidak menahan Ringkasan
      return <p>🔔 {jumlah} notifikasi baru</p>
    }
    
    export default function DashboardPage() {
      return (
        <main className="space-y-4 p-6">
          <h1 className="text-2xl font-bold">Dashboard</h1>
          <Suspense fallback={<p>Memuat ringkasan...</p>}>
            <Ringkasan />
          </Suspense>
          <Suspense fallback={<p>Memuat notifikasi...</p>}>
            <Notifikasi />
          </Suspense>
        </main>
      )
    }
    // → npm run build menandai /dashboard dengan ◐ (Partial Prerender)
    // → judul + dua fallback dikirim seketika, ringkasan ±0,8 dtk, notifikasi ±2 dtk
    

    Fetch berurutan yang memang bergantung, dengan streaming agar tidak menahan seluruh halaman:

    // app/artis/[id]/page.jsx
    import { Suspense } from 'react'
    
    async function getArtis(id) {
      const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
      return res.json()
    }
    
    async function getAlbum(artisId) {
      const res = await fetch(`https://jsonplaceholder.typicode.com/albums?userId=${artisId}&_limit=3`)
      return res.json()
    }
    
    async function DaftarAlbum({ artisId }) {
      const album = await getAlbum(artisId) // butuh artisId → memang harus setelah getArtis
      return <ul>{album.map((a) => <li key={a.id}>{a.title}</li>)}</ul>
    }
    
    async function Profil({ params }) {
      const { id } = await params
      const artis = await getArtis(id)
      return (
        <>
          <h1 className="text-2xl font-bold">{artis.name}</h1>
          <Suspense fallback={<p>Memuat album...</p>}>
            <DaftarAlbum artisId={artis.id} />
          </Suspense>
        </>
      )
    }
    
    export default function ArtisPage({ params }) {
      return (
        <main className="p-6">
          <Suspense fallback={<p>Memuat profil...</p>}>
            <Profil params={params} />
          </Suspense>
        </main>
      )
    }
    

    Meneruskan Promise ke Client Component:

    // app/komentar/page.jsx, Server Component
    import { Suspense } from 'react'
    import DaftarKomentar from './DaftarKomentar'
    
    async function getKomentar() {
      const res = await fetch('https://jsonplaceholder.typicode.com/comments?_limit=3')
      return res.json()
    }
    
    export default function KomentarPage() {
      const komentar = getKomentar() // TIDAK di-await
      return (
        <Suspense fallback={<p>Memuat komentar...</p>}>
          <DaftarKomentar komentar={komentar} />
        </Suspense>
      )
    }
    
    // app/komentar/DaftarKomentar.jsx
    'use client'
    
    import { use } from 'react'
    
    export default function DaftarKomentar({ komentar }) {
      const data = use(komentar) // menunggu Promise; selama menunggu, fallback Suspense tampil
      return <ul>{data.map((k) => <li key={k.id}>{k.name}</li>)}</ul>
    }
    

    Kesalahan umum

    1. Memakai useEffect + useState untuk data awal halaman. Di App Router, ambil data di Server Component. Konten langsung ada di HTML, tanpa kedipan loading.
    2. await berurutan untuk request yang tidak saling bergantung. Terjadi waterfall. Gunakan Promise.all.
    3. Fetch tanpa cache di luar <Suspense> saat Cache Components aktif. Build gagal. Bungkus dengan Suspense, atau cache dengan "use cache" kalau datanya boleh dibagi ke semua user.
    4. Memanggil Route Handler sendiri dari Server Component (fetch('http://localhost:3000/api/produk')). Itu memutar lewat jaringan tanpa alasan dan sering gagal saat build. Panggil fungsi datanya langsung.
    5. Tidak memeriksa res.ok. fetch tidak melempar error untuk status 404/500. Cek res.ok dan tangani: tampilkan pesan, notFound(), atau throw agar ditangkap error.js.

    Latihan

    1. Mudah, Buat halaman /todo yang mengambil 5 todo dari https://jsonplaceholder.typicode.com/todos?_limit=5 dan menampilkan judulnya dengan tanda ✅ untuk yang completed.
    2. Sedang, Buat halaman /profil/[id] yang mengambil user dan post miliknya (/users/{id} dan /posts?userId={id}) secara paralel dengan Promise.all. Tampilkan nama user dan jumlah post-nya.
    3. Menantang, Ubah latihan 2 agar nama user tampil duluan, lalu daftar judul post di-stream di <Suspense> terpisah. Jelaskan kenapa versi ini terasa lebih cepat bagi user, walaupun total waktunya mirip.
    Lihat solusi

    1.

    // app/todo/page.jsx
    import { Suspense } from 'react'
    
    async function DaftarTodo() {
      const res = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5')
      const todos = await res.json()
      return (
        <ul>
          {todos.map((t) => (
            <li key={t.id}>{t.completed ? '✅' : '⬜'} {t.title}</li>
          ))}
        </ul>
      )
    }
    
    export default function TodoPage() {
      return (
        <main className="p-6">
          <h1 className="text-2xl font-bold">Todo</h1>
          <Suspense fallback={<p>Memuat...</p>}>
            <DaftarTodo />
          </Suspense>
        </main>
      )
    }
    

    2.

    // app/profil/[id]/page.jsx
    import { Suspense } from 'react'
    
    const API = 'https://jsonplaceholder.typicode.com'
    
    async function Isi({ params }) {
      const { id } = await params
      const [user, posts] = await Promise.all([
        fetch(`${API}/users/${id}`).then((r) => r.json()),
        fetch(`${API}/posts?userId=${id}`).then((r) => r.json()),
      ])
      return <p>{user.name} sudah menulis {posts.length} post.</p>
    }
    
    export default function ProfilPage({ params }) {
      return (
        <Suspense fallback={<p>Memuat profil...</p>}>
          <Isi params={params} />
        </Suspense>
      )
    }
    // → /profil/1 → "Leanne Graham sudah menulis 10 post."
    

    3.

    // app/profil/[id]/page.jsx
    import { Suspense } from 'react'
    
    const API = 'https://jsonplaceholder.typicode.com'
    
    async function NamaUser({ id }) {
      const user = await fetch(`${API}/users/${id}`).then((r) => r.json())
      return <h1 className="text-2xl font-bold">{user.name}</h1>
    }
    
    async function DaftarPost({ id }) {
      const posts = await fetch(`${API}/posts?userId=${id}`).then((r) => r.json())
      return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
    }
    
    async function Isi({ params }) {
      const { id } = await params
      return (
        <>
          <Suspense fallback={<p>Memuat nama...</p>}>
            <NamaUser id={id} />
          </Suspense>
          <Suspense fallback={<p>Memuat post...</p>}>
            <DaftarPost id={id} />
          </Suspense>
        </>
      )
    }
    
    export default function ProfilPage({ params }) {
      return (
        <main className="p-6">
          <Suspense fallback={<p>Memuat...</p>}>
            <Isi params={params} />
          </Suspense>
        </main>
      )
    }
    

    Kedua request tetap berjalan bersamaan (dua komponen async dirender paralel), tapi user langsung melihat bagian yang sudah siap. Waktu sampai konten pertama muncul lebih pendek, dan itulah yang dirasakan sebagai "cepat".

    Ringkasan

    Video pendamping

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

    Next.js 15 Tutorial - 68 - Fetching Data in Server ComponentsCodevolution · Bahasa InggrisBuka di YouTube

    Kuis pemahaman

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