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:
- Tidak di-cache secara default. Setiap request diulang kecuali kamu meng-cache-nya (modul berikutnya).
- Request identik di satu pohon render di-memoize (digabung). Kalau
HeaderdanPagesama-sama memanggilfetch('/api/user')dalam satu request, hanya satu request yang benar-benar dikirim. Jadi kamu bisa fetch di komponen yang membutuhkan tanpa harus mengoper props berlapis-lapis.
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
- Memakai
useEffect+useStateuntuk data awal halaman. Di App Router, ambil data di Server Component. Konten langsung ada di HTML, tanpa kedipan loading. awaitberurutan untuk request yang tidak saling bergantung. Terjadi waterfall. GunakanPromise.all.- 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. - 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. - Tidak memeriksa
res.ok.fetchtidak melempar error untuk status 404/500. Cekres.okdan tangani: tampilkan pesan,notFound(), atauthrowagar ditangkaperror.js.
Latihan
- Mudah, Buat halaman
/todoyang mengambil 5 todo darihttps://jsonplaceholder.typicode.com/todos?_limit=5dan menampilkan judulnya dengan tanda ✅ untuk yangcompleted. - Sedang, Buat halaman
/profil/[id]yang mengambil user dan post miliknya (/users/{id}dan/posts?userId={id}) secara paralel denganPromise.all. Tampilkan nama user dan jumlah post-nya. - 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
- Server Component boleh
async; ambil data denganfetchatau langsung dari database tanpa API perantara. fetchdi server tidak di-cache secara default, tapi request identik dalam satu render di-memoize; untuk ORM pakaiReact.cache.- Request yang tidak saling bergantung dijalankan paralel dengan
Promise.all. <Suspense>membuat bagian halaman tampil begitu siap (streaming); dengan Cache Components, data tanpa cache wajib di dalam Suspense.connection()menandai komponen yang harus dirender per request;use()membaca Promise yang dioper dari server ke client.