React Server Components di produksi: pola dan jebakan
React Server Components di produksi: pola yang teruji, anti-pola umum, dan strategi debugging untuk aplikasi Next.js 15 yang andal.

React Server Components (RSC) secara mendasar mengubah cara kerja rendering di sisi server pada Next.js 15, namun penerapan di produksi mengungkap jebakan yang tidak selalu dibahas oleh dokumentasi resmi. Artikel ini menguraikan pola yang berhasil, pola yang rusak, dan cara mendiagnosis masalah sebelum mencapai produksi.
Server Component berjalan secara eksklusif di server dan mengirim nol JavaScript ke browser. Client Component (ditandai dengan "use client") berjalan di kedua sisi. Aturannya: jaga Client Components sekecil dan serendah mungkin di pohon komponen.
Batas server-client: memahami pola boundary
Jebakan RSC paling umum melibatkan batas antara Server dan Client Components. Begitu sebuah komponen membawa direktif "use client", semua anak yang diimpornya juga menjadi Client Components, meski tanpa direktif tersebut.
import { ProductDetails } from './ProductDetails'
import { AddToCartButton } from './AddToCartButton'
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const product = await getProduct(id)
return (
<div>
{/* Server Component: akses langsung ke DB */}
<ProductDetails product={product} />
{/* Client Component: interaktivitas terisolasi */}
<AddToCartButton productId={product.id} price={product.price} />
</div>
)
}'use client'
import { useState } from 'react'
export function AddToCartButton({ productId, price }: { productId: string; price: number }) {
const [adding, setAdding] = useState(false)
async function handleAdd() {
setAdding(true)
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ productId, quantity: 1 }),
})
setAdding(false)
}
return (
<button onClick={handleAdd} disabled={adding}>
{adding ? 'Menambahkan...' : `Tambah ke keranjang — $${price}`}
</button>
)
}Pola kunci: meneruskan data sebagai props yang dapat diserialisasi dari Server Component ke Client Component. Fungsi, kelas, dan objek Date tidak bisa melintasi batas ini.
Anti-pola: pembungkus Client Component yang tidak perlu
Kesalahan yang sering terjadi adalah membuat Client Component yang membungkus anak-anak Server Component, sehingga memaksa seluruh subtree menjadi sisi klien.
'use client'
import { useState } from 'react'
// Seluruh konten anak menjadi sisi klien
export function PageWrapper({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('light')
return (
<div className={theme}>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Ganti tema
</button>
{children}
</div>
)
}Solusinya: meneruskan Server Components sebagai children (pola slot). Anak-anak yang diteruskan sebagai props tetap menjadi Server Components meskipun induknya adalah Client Component. Kode di atas berfungsi dengan benar selama children berasal dari induk Server Component.
import { PageWrapper } from './PageWrapper'
import { HeavyServerContent } from './HeavyServerContent'
export default function Layout() {
return (
<PageWrapper>
{/* Tetap Server Component meski ada pembungkus klien */}
<HeavyServerContent />
</PageWrapper>
)
}Pola komposisi ini mempertahankan manfaat rendering server untuk konten berat sekaligus memungkinkan interaktivitas pada level pembungkus.
Penanganan data asinkron: pola fetch dalam komponen
React 19 dan Next.js 15 mendukung async/await langsung di Server Components. Pola ini menyederhanakan pengambilan data dibandingkan pendekatan getServerSideProps yang lama.
import { cache } from 'react'
// Mengeliminasi panggilan identik dalam render yang sama
const getUser = cache(async (userId: string) => {
const res = await fetch(`https://api.example.com/users/${userId}`, {
next: { revalidate: 3600 }, // Cache selama 1 jam
})
if (!res.ok) throw new Error('User not found')
return res.json()
})
export default async function UserProfile({ userId }: { userId: string }) {
const user = await getUser(userId)
return (
<section>
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>Anggota sejak {new Date(user.createdAt).toLocaleDateString('id-ID')}</p>
</section>
)
}Tiga poin penting:
- Fungsi
cache()React mengeliminasi panggilan identik selama satu render server next: { revalidate }mengontrol durasi cache di sisi Next.js- Error pada Server Component asinkron memicu
error.tsxterdekat
Jebakan serialisasi: yang tidak bisa melintasi batas
Data yang dipertukarkan antara Server dan Client Components harus dapat diserialisasi ke JSON. Berikut adalah hal-hal yang menyebabkan error senyap atau crash.
// JEBAKAN: meneruskan tipe yang tidak dapat diserialisasi
// Fungsi — tidak bekerja
<ClientComp onSubmit={async (data) => { /* server action */ }} />
// Gunakan Server Action yang diimpor sebagai gantinya
import { submitForm } from '@/lib/actions/form'
<ClientComp onSubmit={submitForm} />
// Objek Date — tidak bekerja
<ClientComp createdAt={new Date()} />
// String ISO — bekerja
<ClientComp createdAt={new Date().toISOString()} />
// Map, Set, RegExp — tidak bekerja
<ClientComp data={new Map([['key', 'value']])} />
// Objek biasa atau array — bekerja
<ClientComp data={{ key: 'value' }} />Server Actions (fungsi yang ditandai "use server") adalah pengecualian: mereka bisa diteruskan sebagai props ke Client Component karena Next.js mengubahnya menjadi endpoint HTTP.
Siap menguasai wawancara React / Next.js Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Streaming dan Suspense: pola pemuatan progresif
Streaming SSR dengan Suspense mengirim HTML ke browser secara progresif. Pola optimal menggunakan boundary Suspense yang granular di sekitar setiap bagian asinkron.
import { Suspense } from 'react'
import { RevenueChart } from './RevenueChart'
import { RecentOrders } from './RecentOrders'
import { UserStats } from './UserStats'
export default function DashboardPage() {
return (
<div className="grid grid-cols-2 gap-6">
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<UserStats />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</div>
)
}Setiap bagian dimuat secara independen. Jika RevenueChart memakan 3 detik dan UserStats 200 ms, statistik muncul seketika tanpa menunggu grafik.
Konten di dalam boundary Suspense dirender di server dan disertakan dalam HTML awal. Crawler melihat seluruh konten. Streaming hanya memengaruhi kecepatan pengiriman ke browser, bukan visibilitas SEO.
Debugging produksi: melacak masalah RSC
Error RSC sering kali samar. Tiga teknik diagnostik bekerja di produksi.
1. Mengidentifikasi ketidakcocokan hidrasi
'use client'
import { useEffect, useState } from 'react'
export function HydrationDebug() {
const [isClient, setIsClient] = useState(false)
useEffect(() => {
setIsClient(true)
}, [])
if (process.env.NODE_ENV !== 'development') return null
return (
<div style={{ position: 'fixed', bottom: 0, right: 0, padding: '4px 8px', fontSize: 12 }}>
{isClient ? 'Client' : 'Server'}
</div>
)
}2. Mencatat payload RSC
Di Next.js 15, aktifkan logging RSC di next.config.ts:
const nextConfig = {
logging: {
fetches: {
fullUrl: true, // Menampilkan URL fetch lengkap
},
},
}
export default nextConfig3. Memeriksa ukuran payload
Payload RSC yang berlebihan (> 128 KB) menurunkan kinerja. Pantau permintaan jaringan dengan content type text/x-component di DevTools.
Pola lanjutan: komposisi dengan Server Actions
Server Actions yang dikombinasikan dengan Server Components menciptakan pola CQRS alami: pembacaan di server (RSC), penulisan melalui actions.
import { getTodos } from '@/lib/services/todo'
import { TodoForm } from './TodoForm'
import { deleteTodo } from '@/lib/actions/todo'
export default async function TodoList() {
const todos = await getTodos()
return (
<div>
<TodoForm />
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.title}
<form action={deleteTodo}>
<input type="hidden" name="id" value={todo.id} />
<button type="submit">Hapus</button>
</form>
</li>
))}
</ul>
</div>
)
}'use server'
import { revalidatePath } from 'next/cache'
import { TodoService } from '@/lib/services/todo'
export async function deleteTodo(formData: FormData) {
const id = formData.get('id') as string
await TodoService.delete(id)
revalidatePath('/todos')
}Panggilan revalidatePath memicu render Server Component yang baru dengan data terbaru, tanpa memuat ulang seluruh halaman.
Untuk persiapan wawancara yang lebih mendalam tentang topik-topik ini, silakan lihat modul Next.js Server Actions dan modul Next.js Data Fetching di SharpSkill. Dokumentasi resmi React memuat seluruh spesifikasi Server Components.
Kesimpulan
- Menjaga Client Components tetap kecil dan terisolasi di bagian bawah pohon komponen
- Menggunakan pola slot (
children) untuk mempertahankan Server Components di dalam pembungkus klien - Selalu memverifikasi serialisabilitas props yang melintasi batas server-client
- Menempatkan boundary Suspense yang granular di sekitar setiap bagian asinkron yang independen
- Memantau ukuran payload RSC di produksi (target < 128 KB)
- Menggabungkan Server Components (pembacaan) dan Server Actions (penulisan) untuk pola CQRS alami
- Menggunakan
cache()React untuk mengeliminasi permintaan dalam satu render server
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Tag
Bagikan
Artikel terkait

React 19: Server Components di Produksi - Panduan Lengkap
Kuasai React 19 Server Components di lingkungan produksi. Arsitektur, pola desain, streaming, caching, dan optimasi untuk aplikasi berkinerja tinggi.

React Compiler di Tahun 2026: Memoization Otomatis dan Pertanyaan Interview
Pelajari React Compiler untuk memoization otomatis, pipeline kompilasi, aturan React, dan pertanyaan interview yang sering muncul di tahun 2026.

React Hooks Tingkat Lanjut: Pola dan Optimasi
Kuasai React Hooks tingkat lanjut dengan pola yang teruji. Custom hooks, useEffect teroptimasi, useMemo, useCallback, dan teknik performa.