Skip to main content
BlogWeb Dev

Perbedaan Server Component dan Client Component Next.js

Perbedaan server component dan client component Next.js untuk pemula: use client, props, pola children, tabel keputusan, dan error hydration yang umum.

Matthews Wong

Software Engineer

Oktober 202610 menit baca
Perbedaan Server Component dan Client Component Next.js

Ringkasan Utama

Perbedaan server component dan client component Next.js: di App Router semua komponen adalah Server Component secara default, dirender di server dan tidak mengirim JavaScript sendiri. Tambahkan use client di baris pertama file hanya untuk bagian yang butuh state, event handler, atau API browser, dan kirim props yang bisa diserialisasi.

Kamu menulis useState di halaman Next.js, lalu build gagal dan error menyuruhmu menambahkan use client. Kamu menurut, dan error hilang. Beberapa menit kemudian kamu mengirim fungsi onClick sebagai prop dari halaman ke komponen tombol, dan muncul error baru soal props yang tidak bisa diserialisasi. Pemula biasanya tersangkut di sini: tahu bahwa ada dua jenis komponen, tetapi belum tahu aturan di balik keduanya.

Panduan ini membawamu lewat enam langkah kecil di aplikasi baru, dari komponen default sampai pola children, hydration, dan kode khusus server. Setiap aturan saya cocokkan dengan dokumentasi resmi Next.js dan React. Contoh di langkah 5 memakai pola yang sama dengan komponen SiteChrome di situs ini, dan fakta lain di panduan ini sama dengan yang dipakai pelajaran gratis di bawah.

Mau melihat kode nyata situs ini, termasuk route game 3D yang baru memuat engine setelah pengunjung setuju? Ikuti pelajaran gratis Belajar Server dan Client Component Next.js dari Kode Nyata di jalur Web app skills, lengkap dengan penjelasan interaktif dan kuis.

Yang kamu butuhkan

Tidak perlu proyek besar. Satu aplikasi kosong sudah cukup untuk mencoba semua langkah.

  • Node.js yang masih didukung dan terminal. Perintah di bawah memakai npm.
  • Aplikasi Next.js baru dengan App Router, dibuat lewat create-next-app pada langkah 1. Terima pilihan default.
  • Editor kode dan browser. Kamu akan membaca HTML mentah dari server lewat curl, bukan hanya melihat inspector.

Langkah 1: Kenali default-nya, semua komponen adalah Server Component

Buat aplikasi dan jalankan. Di folder app, file seperti layout.tsx dan page.tsx adalah Server Component karena tidak ada direktif apa pun di atasnya. Dokumentasi Next.js menyebut layout dan page secara default adalah Server Component. Server Component dirender di server dan browser hanya menerima hasil render-nya, jadi kode itu bisa mengambil data dari database atau memakai API key tanpa membocorkannya.

npx create-next-app@latest lab
cd lab
npm run dev

# app/page.tsx has no "use client" line, so it is a Server Component.
# It may be async, read a database, or use a secret. It may NOT call
# useState or useEffect, and it may NOT have an onClick.

Harganya ada satu: Server Component tidak punya state dan tidak punya event handler. Kode server hanya dijalankan lalu hasilnya dikirim, tidak ada komponen yang tetap hidup di browser untuk menyimpan state. Hampir semua error pemula berasal dari aturan ini, jadi hafalkan dulu sebelum lanjut.

Langkah 2: Buat Client Component pertama dengan use client

Sekarang buat satu file kecil yang memang butuh state, yaitu tombol like. Tambahkan use client di baris paling atas, sebelum import apa pun. Dokumentasi React menyebut direktif ini harus ada di awal file, di atas import dan kode lain (komentar boleh), dengan tanda kutip tunggal atau ganda, bukan backtick. Direktif yang ditaruh di bawah import tidak dianggap.

// app/like-button.tsx
// Wrong: the directive is below the imports, so it is not at the top of the file
import { useState } from "react";
"use client";

// Right: the very first line (comments are fine above it), before any import
"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>;
}

Perhatikan bahwa use client berlaku untuk seluruh modul dan tidak bisa ditaruh di dalam fungsi atau komponen. Kamu hanya perlu menambahkannya di file yang komponennya dirender langsung oleh Server Component. File lain yang diimpor oleh file itu ikut masuk ke bundle browser tanpa perlu direktif sendiri.

Langkah 3: Pakai dari halaman server dan kirim props yang aman

Halaman tetap Server Component. Ia mengimpor tombol dan mengirim angka biasa sebagai prop. Setelah itu jalankan curl untuk melihat HTML yang dikirim server. Judul halaman sudah ada di dalamnya, padahal tombolnya Client Component. Itu karena pada first load, Client Component juga di-prerender menjadi HTML di server, lalu JavaScript men-hydrate untuk memasang event handler. Client Component bukan berarti hanya di browser.

// app/page.tsx (Server Component: no directive needed)
import LikeButton from "./like-button";

export default function Page() {
  return (
    <main>
      <h1>Server text, rendered once</h1>
      {/* Right: a number is serializable */}
      <LikeButton start={3} />
      {/* Wrong: a function cannot cross the server-to-client boundary
      <LikeButton start={3} onDone={() => {}} /> */}
    </main>
  );
}
# Check the HTML the server sent, not the inspector:
curl -s http://localhost:3000 | grep -o 'Server text, rendered once'

Props yang melewati batas use client harus bisa diserialisasi supaya React bisa mengirimnya dari server ke browser. Menurut dokumentasi React, yang boleh antara lain string, angka, boolean, null, Date, array, Map, Set, plain object, elemen JSX, dan Promise. Fungsi biasa dan instance class tidak boleh. Karena itu di situs ini PlayShell menerima string locale dan sebuah angka, bukan callback seperti onClose.

Jangan menyelesaikan error props dengan menaruh use client di halaman. Itu hanya memindahkan seluruh halaman, beserta semua import-nya, ke bundle browser. Pindahkan handler ke dalam Client Component dan kirim hanya data biasa dari server.

Langkah 4: Letakkan batas use client sedalam mungkin

Direktif use client menarik semua yang diimpor file itu ke bundle klien, jadi semakin tinggi kamu menaruhnya, semakin banyak JavaScript yang dikirim. Contoh di dokumentasi Next.js: layout berisi logo dan link navigasi yang statis, ditambah satu kolom pencarian yang interaktif. Hanya kolom pencarian yang perlu menjadi Client Component. Logo dan layout tetap di server.

// app/layout.tsx stays a Server Component
import Search from "./search"; // "use client" lives inside search.tsx
import Logo from "./logo"; // plain Server Component, ships no JS of its own

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Logo />
        <Search />
      </nav>
      <main>{children}</main>
    </>
  );
}

// Wrong: "use client" on layout.tsx would pull Logo and every import
// of the layout into the browser bundle for no reason.

Kebiasaan yang bagus: mulai dari Server Component untuk semuanya, lalu pindahkan potongan interaktif ke file kecil sendiri satu per satu. Jangan membalik urutannya dengan menandai halaman sebagai klien lalu mencoba memperbaikinya belakangan.

Langkah 5: Campur keduanya dengan pola children

Kadang bagian interaktif perlu membungkus konten yang dirender server, misalnya modal yang buka tutup lewat state tetapi isinya keranjang belanja dari database. Kirim Server Component sebagai children ke Client Component. Client Component hanya menyediakan slot. Menurut dokumentasi Next.js, komponen yang dikirim sebagai children atau prop lain tidak diimpor ke graf modul klien, dirender di server lebih dulu, dan dikirim ke Client Component sebagai hasil render.

// app/ui/modal.tsx: the client part only owns the open/closed state
"use client";

import { useState } from "react";

export default function Modal({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(false);
  return (
    <div>
      <button onClick={() => setOpen(!open)}>Toggle</button>
      {open && <div>{children}</div>}
    </div>
  );
}

// app/page.tsx: Cart stays a Server Component, rendered ahead of time
import Modal from "./ui/modal";
import Cart from "./ui/cart";

export default function Page() {
  return (
    <Modal>
      <Cart />
    </Modal>
  );
}

Pola yang sama dipakai komponen SiteChrome di situs ini. Ia harus membaca pathname sehingga menjadi Client Component, tetapi header dan footer dirender di layout server lalu dikirim sebagai props bertipe ReactNode. SiteChrome hanya memutuskan apakah akan menampilkannya.

// app/components/SiteChrome.tsx (this site): a Client Component whose
// props are ReactNode slots filled in by the server layout
interface SiteChromeProps {
  header: ReactNode;
  footer: ReactNode;
  /** Floating helpers: the chatbot, the selection explainer. */
  extras: ReactNode;
  children: ReactNode;
}

Hal yang sama berlaku untuk provider seperti theme. React context tidak didukung di Server Component, jadi buat provider kecil dengan use client yang menerima children, lalu pasang di layout server. Dokumentasi menyarankan memasang provider sedalam mungkin: bungkus children, bukan seluruh elemen html.

Langkah 6: Hindari error hydration dan kode server yang bocor

Karena Client Component dirender dua kali, sekali di server dan sekali di browser saat hydration, hasil keduanya harus sama. Dokumentasi Next.js mencantumkan penyebab umum: memakai Date, window, atau localStorage langsung di logika render, memeriksa typeof window, dan HTML yang bersarang salah seperti p di dalam p. Perbaikannya adalah merender hal yang sama di kedua sisi, lalu mengisi nilai khusus browser di useEffect.

"use client";

import { useEffect, useState } from "react";

// Wrong: the server renders one thing, the browser's first render another,
// so React reports a hydration mismatch
export function ClockWrong() {
  return <p>{new Date().toLocaleTimeString()}</p>;
}

// Right: render the same placeholder on both sides, fill in after hydration
export function ClockRight() {
  const [now, setNow] = useState("");
  useEffect(() => {
    setNow(new Date().toLocaleTimeString());
  }, []);
  return <p>{now || "..."}</p>;
}

Untuk contoh error lengkap dan cara membacanya, lihat panduan memperbaiki error hydration Next.js. Dua jalan keluar lain dari dokumentasi: memuat komponen dengan next/dynamic dan ssr false di dalam Client Component, atau suppressHydrationWarning untuk satu elemen seperti timestamp. Yang kedua hanya jalan darurat, jangan dipakai berlebihan.

Kapan pakai yang mana: tabel keputusan

Tanyakan satu hal pada setiap komponen: apakah ia butuh sesuatu yang hanya ada di browser? Kalau tidak, biarkan Server Component. Daftar di bawah mengikuti panduan di dokumentasi Next.js.

Kamu butuhPilihAlasan singkat
useState, onClick, onChangeClient ComponentServer Component tidak bisa menyimpan state atau menerima event.
useEffect atau custom hookClient ComponentLogika lifecycle berjalan di browser.
window, localStorage, navigator.geolocationClient ComponentAPI khusus browser tidak ada di server.
Mengambil data dari database atau APIServer ComponentDekat dengan sumber data, dan hasilnya tidak menambah JavaScript.
API key, token, dan secret lainServer ComponentTidak dikirim ke browser.
Teks artikel dan konten statisServer ComponentMengurangi JavaScript dan membuat teks ada di HTML pertama.

Error umum dan cara memperbaikinya

Enam gejala ini yang paling sering membuat pemula berhenti. Cocokkan pesan yang kamu lihat dengan kolom pertama.

GejalaPenyebabPerbaikan
Error bahwa React hook hanya bisa dipakai di Client ComponentFile memakai useState atau useEffect tetapi tidak punya use client, jadi dianggap Server Component.Pindahkan bagian interaktif ke file kecil dengan use client di baris pertama. Halamannya tetap di server.
Props dianggap tidak bisa diserialisasiFungsi dikirim dari Server Component ke Client Component.Kirim data biasa dan definisikan handler di dalam Client Component.
Error hydration: Text content does not match server-rendered HTMLHasil render server berbeda dari render pertama di browser, misalnya karena Date, window, atau HTML bersarang salah.Render nilai yang sama di kedua sisi, lalu isi nilai khusus browser di useEffect.
Error bahwa ssr false tidak boleh dengan next/dynamic di Server ComponentOpsi ssr false hanya didukung di Client Component.Pindahkan import dinamis ke file yang diawali use client.
Pemanggilan data gagal diam-diam di browserHelper yang membaca secret diimpor ke Client Component. Variabel tanpa awalan NEXT_PUBLIC_ diganti string kosong di browser.Tambahkan import server-only di bagian atas helper supaya muncul error saat build.
Komponen library pihak ketiga error di Server ComponentLibrary memakai state tetapi modulnya belum punya use client.Bungkus di file kecil sendiri yang diawali use client, lalu impor pembungkus itu.
// lib/data.ts: fails the build if a Client Component ever imports it
import "server-only";

export async function getData() {
  const res = await fetch("https://api.example.com/data", {
    headers: { authorization: process.env.API_KEY ?? "" },
  });
  return res.json();
}

// Wrong: next/dynamic with ssr: false inside a Server Component
// Right: put that dynamic import inside a file that starts with "use client"

Checklist sebelum kamu lanjut

  • Halaman dan layout tetap Server Component, tanpa use client di atasnya.
  • use client ada di baris pertama file, hanya di file kecil yang memang butuh state, effect, atau API browser.
  • Props yang melewati batas hanya berisi data biasa, tanpa fungsi.
  • Konten server yang perlu dibungkus komponen interaktif dikirim lewat children.
  • Kode yang membaca secret punya import server-only, dan nilai browser seperti Date atau window dipindah ke useEffect.

Belajar lebih dalam

Panduan ini fokus pada langkah praktis. Untuk alur lengkapnya, termasuk bagaimana route game di situs ini menahan engine 3D sampai pengunjung setuju dan kenapa teks halaman tidak boleh di balik skeleton loading, buka pelajaran Belajar Server dan Client Component Next.js dari Kode Nyata. Kalau nanti kamu mulai memakai form dan mutasi data di sisi server, baca juga keamanan Server Actions di Next.js, karena batas server dan klien juga menentukan apa yang aman dilakukan.

Aturan yang bisa kamu bawa: mulai dari Server Component, tambahkan use client hanya di potongan terkecil yang butuh browser, dan kirim lewat batas itu hanya data yang bisa diserialisasi.

FAQ

Pertanyaan Umum

Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.

Apa perbedaan server component dan client component di Next.js?

Server Component dirender di server dan hanya mengirim hasil render-nya ke browser, jadi tidak membawa JavaScript sendiri. Client Component ditandai dengan use client dan bisa memakai state, effect, serta API browser. Di App Router, layout dan page secara default adalah Server Component.

Kenapa Next.js menyuruh saya menambahkan use client saat memakai useState?

Server Component tidak pernah dikirim ke browser, jadi tidak bisa menyimpan state atau menjalankan effect. Hook seperti useState dan useEffect hanya bekerja di Client Component. Pindahkan bagian interaktifnya ke file kecil yang diawali use client dan biarkan halamannya tetap di server.

Apakah use client berarti komponen hanya berjalan di browser?

Tidak. Pada first load, Client Component juga di-prerender menjadi HTML di server, lalu di-hydrate di browser agar event handler terpasang. Use client mengubah apa yang dikirim sebagai JavaScript, bukan ada tidaknya HTML awal.

Bisakah saya mengirim fungsi dari server component ke client component?

Tidak, kalau berupa fungsi biasa. Props yang melewati batas use client harus bisa diserialisasi, dan fungsi biasa tidak bisa. Definisikan handler di dalam Client Component dan kirim hanya data seperti string, angka, atau plain object.

Kenapa muncul error hydration di Next.js?

HTML yang dirender server berbeda dari render pertama di browser. Penyebab umum adalah memakai Date, window, atau localStorage di logika render dan HTML bersarang salah seperti p di dalam p. Render nilai yang sama di kedua sisi, lalu isi nilai khusus browser di useEffect.