Belajar Server dan Client Component Next.js dari Kode Nyata
Belajar server dan client component Next.js dengan kode nyata: apa yang berjalan di mana, batas use client, pola children, dan apa yang dikirim ke browser.
Oleh Matthews WongDitinjau pada
Poin utama
Di App Router Next.js setiap komponen secara default adalah Server Component: dirender di server dan tidak mengirim JavaScript sendiri. use client di baris pertama file menandai sebuah batas, dan file itu beserta importnya masuk ke bundle browser agar bisa memakai state, effect, dan API browser.
Setelah pelajaran ini, Anda bisa
- Menentukan per komponen apakah ia hanya berjalan di server atau juga di browser.
- Menyisipkan konten hasil render server ke dalam client component lewat children.
- Menjaga kode berat di luar first load, dan teks halaman tetap ada di HTML dari server.
Ikuti diagramnya langkah demi langkah
Tekan Putar, atau melangkah dengan panah. Setiap langkah juga ditulis di bawah diagram.
Satu halaman, dua lingkungan: route /play
Server merender teks landing menjadi HTML, PlayShell melewati batas use client dan di-hydrate di browser, dan engine 3D baru diunduh setelah pengunjung menyetujui.
Tombol: ← → langkah · Home / End · K putar atau jeda
Halaman play adalah Server Component. Teks landing-nya tetap di server, gerbang PlayShell adalah Client Component, dan game 3D adalah chunk lazy yang belum diunduh siapa pun.
page.tsx hanya berjalan di server. Ia membaca messages dan merender PlayLanding, sebuah Server Component, sehingga kode komponen itu tidak dikirim ke browser.
Tebak langkah berikutnya Berikutnya PlayShell, yang diberi use client. Apa yang diterima browser untuknya? (Hanya HTML hasil render-nya · JavaScript-nya, agar bisa berjalan)PlayShell diawali use client. File itu dan semua yang diimpornya masuk ke bundle klien, dan props-nya harus bisa diserialisasi: di sini string locale dan sebuah angka.
"use client";Browser menerima HTML jadi yang sudah berisi teks landing, plus payload yang menandai posisi PlayShell. Crawler dan ponsel lambat bisa membacanya sebelum skrip apa pun berjalan.
Tebak langkah berikutnya Pengunjung belum menyetujui pemberitahuan. Apakah engine 3D diunduh sekarang? (Ya, bersama halaman · Tidak, menunggu pengunjung)JavaScript men-hydrate PlayShell, yang menampilkan pemberitahuan. Game berada di balik next/dynamic dengan ssr false, jadi chunk engine belum diunduh sampai pengunjung menyetujui.
Setelah pengunjung menyetujui, cabang berbasis state klien me-mount Game dan browser mengunduh chunk lazy itu. Baru sekarang bobot engine sampai ke pengunjung ini.
Apa yang berjalan di server, dan apa yang dikirim ke browser?
Di App Router, layout dan page secara default adalah Server Component. Server Component dirender lebih dulu, di lingkungan yang terpisah dari browser, baik sekali saat build maupun untuk setiap request. Browser tidak menerima kodenya, hanya hasil render-nya, sehingga kode server bisa mengakses database atau memakai secret tanpa membocorkan keduanya.
Baca selengkapnya (2)Tutup
Gunakan Client Component saat Anda butuh state dan event handler, logika lifecycle seperti useEffect, API khusus browser seperti localStorage atau window, atau custom hook. Server Component tidak bisa memakai useState karena tidak pernah dikirim ke browser untuk menyimpan state. Satu aturan itu menjelaskan sebagian besar error yang dialami pemula.
Salah kaprah yang umum: Client Component bukan berarti hanya di klien. Pada first load, Next.js memakai Client Component dan payload Server Component untuk men-prerender HTML, lalu JavaScript men-hydrate Client Component untuk memasang event handler. Pengguna melihat HTML sungguhan lebih dulu. Navigasi berikutnya hanya mengambil payload, dan Client Component dirender di browser.
Apa sebenarnya yang dilakukan direktif use client?
Tulis use client di baris paling atas file, sebelum import. Direktif itu mendeklarasikan batas antara graf modul server dan klien. Setiap modul yang diimpor file itu, dan setiap komponen yang dirender langsung olehnya, masuk ke bundle klien. Direktif tidak perlu diulang di tiap file anak, dan hanya perlu di file yang komponennya dirender langsung oleh Server Component.
Baca selengkapnya (2)Tutup
Props yang melewati batas harus bisa diserialisasi, agar React dapat mengirimnya dari server ke browser. String, angka, objek berisi nilai biasa: boleh. Fungsi: tidak boleh. Itulah sebabnya PlayShell di situs ini menerima string locale dan jumlah byte, bukan callback seperti onClose.
Karena direktif ini menarik seluruh subtree, letakkan sedalam mungkin. Dokumentasi Next.js mencontohkannya dengan layout yang sebagian besar berisi logo dan tautan tetapi punya satu kolom pencarian: hanya kolom pencarian yang menjadi Client Component, sisanya tetap di server.
Bagaimana mencampur keduanya? Kirim konten server lewat children
Anda bisa mengirim Server Component sebagai prop, paling sering children, ke Client Component. Client Component hanya menyediakan slot, misalnya modal yang mengubah visibilitas dengan state. Server Component dirender di server lebih dulu, dan payload membawa hasil render-nya plus penanda tempat Client Component berada. Komponen yang dikirim tidak diimpor ke graf modul klien, jadi kodenya tidak ikut terkirim.
Baca selengkapnya (2)Tutup
Bentuk yang sama berlaku untuk provider. React context tidak didukung di Server Component, jadi provider adalah Client Component kecil yang menerima children, dan layout server yang merendernya. Pasang provider sedalam mungkin: bungkus children, bukan seluruh elemen html, supaya Next.js tetap bisa mengoptimalkan bagian statis.
SiteChrome di situs ini adalah contoh nyatanya. Ia harus membaca pathname, jadi ia Client Component. Namun header, footer, dan pembantu mengambang dikirim sebagai props dari layout server, sehingga SiteChrome hanya memutuskan apakah akan menampilkannya.
Bagaimana situs ini memisahkan kode berat dari teks halaman?
Kode berat ditaruh di balik next/dynamic. Dokumentasi menyebutnya gabungan React.lazy dan Suspense, dan lazy loading berlaku untuk Client Component. Opsi ssr false hanya bekerja di dalam Client Component. Itulah sebabnya engine 3D diimpor di PlayShell, sebuah Client Component, dan baru di-mount setelah pengunjung menyetujui pemberitahuan. Sebuah skrip build menegakkannya: three hanya boleh diimpor di folder game, dan tidak ada route yang boleh mencapai engine lewat import statis.
Baca selengkapnya (2)Tutup
Teks halaman berjalan sebaliknya: dirender langsung, tidak pernah di balik status loading. File loading.js membungkus halaman dengan batas Suspense. React lalu mengalirkan konten asli di elemen tersembunyi dan menukarnya dengan skrip inline, sehingga pembaca yang tidak menjalankan skrip melihat fallback di main dan artikelnya hanya di dalam elemen tersembunyi. Dokumentasi Next.js menyebut streaming tidak merusak SEO untuk crawler yang merender halaman, tetapi situs ini memilih aturan yang lebih ketat.
Jadi situs ini tidak punya file loading di route mana pun, dan tidak memakai opsi loading pada import dinamis yang membawa teks. Satu-satunya kartu loading, GameLoading, hanya tampil di overlay game setelah pengunjung menyetujui, yaitu cabang berbasis state klien, sehingga tidak pernah ada di HTML dari server.
Konfigurasi nyata di balik pelajaran ini
Kutipan dari repositori yang membangun dan men-deploy situs ini, apa adanya.
// The engine chunk (three, fiber, drei, bvh) only ever loads here, and only
// after the visitor accepts — the whole point of the notice gate.
const Game = dynamic(() => import("@/app/components/game/Game"), {
ssr: false,
loading: () => <GameLoading />,
});Batas lazy-nya. PlayShell adalah Client Component, jadi ssr: false sah di sini. Komentarnya menyebut maksudnya: chunk engine baru dimuat setelah pengunjung menyetujui. Kartu loading aman karena Game hanya di-mount di cabang berbasis state klien, setelah persetujuan.
{/* The landing: stills, premise, steps, facts. No client JavaScript. */}
<PlayLanding
locale={pageLocale}
firstLoadBytes={manifest.firstLoadBytes}
/>
{/* The gate. Before acceptance this is the notice card; after it, the
fixed overlay that is the game. The hero button jumps here. */}
<div className="mt-12">
<NextIntlClientProvider messages={stripToEssentialBlogPosts(messages)}>
<PlayShell
locale={pageLocale}
firstLoadBytes={manifest.firstLoadBytes}
/>
</NextIntlClientProvider>
</div>Halaman server-nya. PlayLanding adalah Server Component tanpa JavaScript klien, jadi teksnya ada di HTML. PlayShell adalah gerbangnya, satu-satunya Client Component di sini. Ia berada di dalam provider yang hanya membawa messages yang dibutuhkan game.
interface SiteChromeProps {
header: ReactNode;
footer: ReactNode;
/** Floating helpers: the chatbot, the selection explainer. */
extras: ReactNode;
children: ReactNode;
}Pola slot. SiteChrome adalah Client Component, tetapi props-nya bertipe ReactNode: layout server yang merender header dan footer lalu menyerahkannya. SiteChrome hanya memutuskan apakah akan menampilkannya.
{/* SiteChrome drops the header, footer and floating helpers on
the immersive /play route; every other route renders them as
before. */}
<SiteChrome
header={<Header />}Separuh lain dari slot. Layout server merender Header dan mengirimkannya sebagai prop. Header sendiri adalah Client Component, jadi cara ini diperbolehkan. Komentarnya mencatat alasannya: /play bersifat imersif dan membuang chrome.
Kesalahan yang benar-benar membuang waktu
State atau event handler di Server Component
Mengimpor useState atau menulis onClick di file tanpa use client akan gagal, karena Server Component tidak pernah dikirim ke browser. Pindahkan hanya bagian interaktifnya ke file kecil sendiri dengan use client, dan biarkan halamannya tetap Server Component.
Fungsi dikirim dari server ke klien
Props yang melewati batas use client harus bisa diserialisasi, dan fungsi tidak bisa. Kirim data biasa, dan definisikan handler di dalam Client Component. Dokumentasi menunjukkan prop onClick yang ditandai tidak bisa diserialisasi.
Kode khusus server diimpor ke file klien
Sebuah modul dipakai bersama di kedua lingkungan, jadi helper data yang membaca secret bisa tidak sengaja diimpor ke Client Component. Hanya variabel berawalan NEXT_PUBLIC_ yang sampai ke browser, sehingga secret berubah menjadi string kosong dan pemanggilan gagal diam-diam. Impor paket server-only di bagian atas helper agar mendapat error saat build.
ssr: false di Server Component
next/dynamic dengan ssr false tidak diperbolehkan di Server Component, dan Next.js menampilkan error. Pindahkan import dinamis ke Client Component, seperti yang dilakukan PlayShell. Dokumentasi menambahkan bahwa ini juga menjaga code splitting di klien tetap berfungsi.
Skeleton loading menyembunyikan teks halaman
loading.js atau opsi loading pada import dinamis membuat React mengirim konten halaman setelah footer di dalam elemen tersembunyi, dengan skeleton di main. Apa pun yang tidak menjalankan skrip hanya membaca skeleton. Situs ini menemukan persis hal itu di setiap halaman, dan memperbaikinya pada 2026-10-13 dengan menghapusnya.
| Server Component | Client Component | |
|---|---|---|
| Cara mendapatkannya | Default di app/ | use client di baris pertama file |
| State, effect, API browser | Tidak tersedia | Tersedia |
| Secret dan akses data langsung | Tidak aman dipakai | Jangan taruh secret di sini |
| JavaScript yang dikirim ke browser | Tidak ada milik sendiri | Kode dan importnya |
| Tempat dirender | Hanya di server | Server untuk HTML awal, lalu browser |
Lakukan di komputer Anda sendiri
Anda butuh Node dan aplikasi Next.js baru. Anda akan membuat satu halaman Server Component dengan satu Client Component kecil, lalu melihat apa yang sebenarnya diterima browser.
Buat aplikasinya lalu jalankan. Terima default-nya, termasuk App Router.
npx create-next-app@latest lab cd lab npm run devTambahkan Client Component yang memegang satu-satunya state di halaman.
// app/like-button.tsx "use client"; import { useState } from "react"; export default function LikeButton({ start }: { start: number }) { const [likes, setLikes] = useState(start); return <button onClick={() => setLikes(likes + 1)}>{likes} likes</button>; }Jadikan halaman sebagai Server Component yang merender teks dan mengirim angka biasa ke tombol.
// app/page.tsx import LikeButton from "./like-button"; export default function Page() { return ( <main> <h1>Server text, rendered once</h1> <LikeButton start={3} /> </main> ); }Lihat page source, bukan inspector. Judul dan teks tombol sudah ada di HTML, padahal tombolnya Client Component.
curl -s http://localhost:3000 | grep -o 'Server text, rendered once'Rusak dengan sengaja: kirim fungsi sebagai prop dari page.tsx, misalnya onDone={() => {}}. Baca error-nya, lalu hapus.
Bungkus tombol dengan slot Client Component yang menerima children, lalu kirim Server Component kedua melaluinya.
// app/panel.tsx "use client"; import { useState, type ReactNode } from "react"; export default function Panel({ children }: { children: ReactNode }) { const [open, setOpen] = useState(false); return ( <div> <button onClick={() => setOpen(!open)}>Toggle</button> {open && children} </div> ); }
Tiga pertanyaan sebelum lanjut
Pilih jawaban untuk melihat kenapa benar atau salah. Tidak ada yang disimpan atau dinilai, jadi tebak saja dengan bebas.
Jawab dalam hati, lalu cek
1.Apakah use client berarti komponen tidak pernah berjalan di server?Lihat jawabannya
Jawaban:Tidak. Pada first load, Client Component di-prerender menjadi HTML di server bersama payload Server Component, lalu di-hydrate di browser. Use client mengubah apa yang dikirim sebagai JavaScript, bukan ada tidaknya HTML awal.
2.Kenapa halaman selain /play tidak mengunduh engine 3D?Lihat jawabannya
Jawaban:Engine berada di balik next/dynamic di PlayShell dan baru di-mount setelah persetujuan. Skrip prebuild juga menggagalkan build jika three diimpor di luar folder game atau ada route yang mencapai engine lewat import statis.
3.Rekan Anda menambahkan skeleton loading ke halaman yang penuh teks. Apa yang Anda katakan?Lihat jawabannya
Jawaban:Render teks secara langsung. Fallback Suspense menaruh skeleton di main dan teks asli di elemen tersembunyi, yang tidak pernah terlihat oleh pembaca tanpa skrip. Umpan balik navigasi datang dari progress bar route.
Bahasan mendalam terkait di situs ini
Sumber
Sumber primer yang diperiksa pada tanggal riset. Ikuti tautannya untuk versi terbaru.