Awal Masalah
Waktu mulai serius pakai Next.js App Router, saya langsung jatuh cinta dengan RSC. Fetch data langsung di komponen, tidak ada loading state yang ribet, bundle size lebih kecil β rasanya seperti masa depan.
Tapi begitu mau tambah animasi, saya mulai ragu.
Hero section saya mau dianimasikan semua β judul masuk dari bawah, teks fade in, gambar slide dari samping. Saya pakai GSAP. Dan GSAP butuh hook useGSAP, yang artinya butuh "use client", yang artinya... seluruh Hero section jadi CSR?
Saya mulai berpikir: apakah saya harus memilih antara animasi yang bagus dan RSC yang benar?
Jawabannya: tidak. Tapi untuk sampai ke sana, saya harus benar-benar paham bagaimana Next.js merender komponen dari awal.
Kesalahpahaman Pertama: CSR "Menular" Lewat Children
Asumsi awal saya: kalau parent-nya CSR, maka children-nya otomatis ikut jadi CSR.
Logisnya masuk akal β kalau sebuah komponen dieksekusi di client, semua yang ada di dalamnya juga dieksekusi di client, kan?
Ternyata tidak sesederhana itu.
// AnimatedSection.tsx β CSR
"use client"
export default function AnimatedSection({ children }) {
useGSAP(() => {
gsap.from(".animate", { opacity: 0, stagger: 0.2 })
})
return <section>{children}</section>
}// page.tsx β RSC
export default function Page() {
return (
<AnimatedSection>
<h1 className="animate">Judul Hero</h1>
<p className="animate">Deskripsi panjang di sini.</p>
</AnimatedSection>
)
}<h1> dan <p> di sini tetap dirender di server. Bukan di client.
Kenapa bisa begitu?
Bagaimana Next.js Sebenarnya Merender Komponen
Untuk benar-benar paham, saya harus mundur dan lihat alurnya dari awal.
1. Request masuk ke server.
Next.js mulai eksekusi dari root β biasanya page.tsx β dan berjalan top-down. Setiap komponen diperiksa: punya "use client" atau tidak?
2. RSC dieksekusi di server, CSR di-skip dulu.
Komponen tanpa "use client" langsung dirender di server. Komponen dengan "use client" dicatat sebagai placeholder β "ini nanti client yang handle."
3. Hasilnya bukan HTML biasa β ini RSC Payload. RSC Payload adalah semacam serialized tree yang berisi HTML hasil render RSC, placeholder di tempat CSR berada, dan referensi ke JS bundle yang dibutuhkan.
4. Browser menerima RSC Payload + HTML awal. HTML langsung ditampilkan (ini yang bikin SEO tetap aman). JS bundle untuk CSR komponen juga ikut dikirim.
5. Hydration β CSR komponen "hidup".
React di browser membaca RSC Payload, mengisi placeholder dengan CSR komponen yang sekarang dieksekusi di client. Di sinilah useGSAP() baru berjalan.
Kunci Pemahamannya: Siapa yang Menulis JSX-nya?
Balik ke contoh tadi. <h1> dan <p> ditulis di page.tsx β file RSC. Artinya server yang bertanggung jawab merender mereka. Hasilnya sudah jadi HTML sebelum masuk ke AnimatedSection.
AnimatedSection hanya menerima hasil render, bukan JSX mentah.
SERVER CLIENT
ββββββββββββββββββββββββββββββββββββββββββββββββββ
Page (RSC)
ββ render <h1> β β
HTML
ββ render <p> β β
HTML
ββ AnimatedSection β βΈοΈ placeholder
AnimatedSection β βΆοΈ dieksekusi
ββ terima children (sudah HTML)
ββ useGSAP() jalan β animasi hidup
CSR menular lewat import, bukan lewat props atau children.
// β Ini yang bikin HeroContent ikut jadi CSR
"use client"
import HeroContent from "./HeroContent" // diimport di dalam CSR file
export default function AnimatedSection() {
return (
<section>
<HeroContent />
</section>
)
}// β
Ini aman β HeroContent tetap RSC
"use client"
export default function AnimatedSection({ children }) {
return <section>{children}</section>
}
// Di page.tsx (RSC):
// <AnimatedSection><HeroContent /></AnimatedSection>Masalah Kedua: i18n dan RSC
Setelah animasi beres, saya ketemu masalah lain: internationalization.
Saya mau implement i18n β bahasa bisa dipilih user. Pikiran pertama saya: simpan di localStorage, baca pakai hook, selesai.
Tapi masalahnya, SSR tidak bisa menunggu nilai dari client. Kalau locale dibaca dari localStorage, server tidak tahu bahasa apa yang harus dipakai saat render. Hasilnya: server kirim konten bahasa default dulu, setelah hydration baru ganti β ada flicker, layout shift, dan SEO berantakan.
Solusinya: data dari user harus dikirim sebelum atau bersamaan dengan request β bukan setelah halaman dimuat.
Ada dua cara yang saya temukan:
Opsi 1: Locale dari URL
mysite.com/en/about
mysite.com/id/about
Ini yang paling clean. Server langsung tahu locale dari URL β tidak perlu cookie, tidak perlu client. Library next-intl pakai pendekatan ini.
Opsi 2: Locale dari Cookie
// middleware.ts
export function middleware(request: NextRequest) {
const locale = request.cookies.get("locale")?.value ?? "en"
request.headers.set("x-locale", locale)
return NextResponse.next()
}Cookie dibaca server sebelum render dimulai. Client set cookie saat user ganti bahasa β request berikutnya server sudah tahu.
Params, Bukan useParams
Konsekuensi dari pendekatan URL-based: di RSC, saya tidak bisa pakai useParams (itu hook, hanya untuk CSR). Gantinya pakai params yang di-pass sebagai props.
// app/[locale]/page.tsx β RSC
export default async function Page({ params }) {
const { locale } = await params
const t = await getDictionary(locale)
return <HeroSection title={t.hero.title} />
}Untuk tree yang dangkal, prop drilling dari sini sudah cukup. Untuk project yang lebih besar, next-intl punya getTranslations yang bisa dipakai di RSC mana pun tanpa perlu drilling:
// Komponen RSC manapun
import { getTranslations } from "next-intl/server"
export default async function HeroSection() {
const t = await getTranslations("hero")
return <h1>{t("title")}</h1>
}Perhatikan: getTranslations (untuk RSC), bukan useTranslations (untuk CSR). Perbedaan kecil, tapi penting.
Apa yang Saya Pelajari
Dari semua ini, ada beberapa hal yang mengubah cara saya berpikir tentang arsitektur Next.js:
RSC bukan tentang "tidak ada JS di client." Ini tentang memindahkan pekerjaan yang tidak perlu ada di client β data fetching, heavy dependencies β ke server. Animasi memang butuh client. Itu wajar.
CSR menular lewat import, bukan children. Kalau ingin isolasi "kerusakan", jangan import komponen besar di dalam CSR file. Pass sebagai children dari RSC.
SSR tidak bisa menunggu client. Apapun yang dibutuhkan server harus tersedia sebelum request β lewat URL, cookie, atau header. Bukan setelah halaman terbuka.
Library i18n pun punya dua mode. react-i18next murni CSR. next-intl punya versi server dan client. Pilih sesuai kebutuhan, jangan asal ikut tutorial yang tidak mempertimbangkan RSC.
Sekarang Hero section saya tetap dirender di server β konten statis, SEO aman, tidak ada layout shift. Tapi animasinya tetap jalan smooth pakai GSAP, karena wrapper animasinya adalah CSR yang menerima children dari RSC.
Bukan pilih salah satu. Keduanya bisa jalan bersamaan β kalau kita paham bagaimana Next.js sebenarnya bekerja.
