kodingup.camp

Beranda/Next.js Mastery: Dasar, Routing & Rendering

Server & Client Component

Modul 6 dari 105 menit bacaServer and Client Components

Isi modul

    Kenapa ini penting

    Ini konsep paling penting di App Router, dan sumber kebingungan terbesar bagi developer yang datang dari React biasa. Di Next.js, setiap komponen secara default adalah Server Component: berjalan di server, tidak dikirim sebagai JavaScript ke browser, dan tidak bisa memakai useState atau onClick. Kalau kamu paham kapan memakai server dan kapan client, aplikasimu lebih cepat, lebih aman, dan lebih sedikit bug.

    Konsep inti

    Dua jenis komponen

    Analogi restoran: Server Component adalah dapur, tempat bahan mentah (database, API key) diolah dan pelanggan tidak pernah masuk. Client Component adalah meja pelanggan, tempat interaksi terjadi: menekan bel, memilih saus, mengobrol. Makanan dimasak di dapur lalu diantar ke meja; bahan mentahnya tetap di dapur.

    Butuh... Pakai
    Ambil data dari database/API Server Component
    Memakai API key, token, password Server Component
    Mengurangi JavaScript yang dikirim ke browser Server Component
    useState, useReducer, useEffect Client Component
    Event handler: onClick, onChange, onSubmit di client Client Component
    API browser: window, localStorage, geolocation Client Component
    Custom hook dan Context Client Component

    "use client" adalah batas, bukan label

    Tulis 'use client' di baris paling atas file (sebelum import). Ini menandai batas antara dunia server dan client. Semua yang di-import dan dirender langsung oleh file itu ikut masuk bundle client. Jadi kamu tidak perlu menulis 'use client' di setiap file anak; cukup di "pintu masuk" bagian interaktif.

    Akibatnya, letakkan 'use client' serendah mungkin di pohon komponen. Jangan jadikan seluruh halaman Client Component hanya karena ada satu tombol suka. Jadikan tombolnya saja Client Component.

    Bagaimana keduanya bekerja

    1. Di server, Server Component dirender menjadi format khusus bernama RSC Payload (React Server Component Payload), berisi hasil render plus "tempat kosong" untuk Client Component dan alamat file JS-nya.
    2. Dari RSC Payload dan Client Component, Next.js membuat HTML awal.
    3. Di browser, HTML tampil duluan (cepat, belum interaktif), lalu JavaScript Client Component dimuat dan di-hydrate: React memasang event handler ke HTML yang sudah ada, sehingga halaman menjadi interaktif.

    Client Component juga dirender di server untuk HTML awal. Bedanya, kodenya juga dikirim ke browser dan dijalankan di sana.

    Meneruskan props dari server ke client

    Server Component boleh merender Client Component dan memberinya props. Syaratnya: props harus bisa diserialisasi (diubah menjadi data yang bisa dikirim lewat jaringan): string, number, boolean, null, array, object biasa, Date, dan Promise. Fungsi tidak bisa dikirim. Kami mencobanya dan build gagal dengan pesan "Event handlers cannot be passed to Client Component props."

    Composition pattern: Server Component sebagai children

    Client Component tidak boleh meng-import Server Component (yang di-import akan ikut menjadi kode client). Tapi Client Component boleh menerima Server Component lewat children atau prop lain. Server Component itu dirender di server, lalu hasilnya "diselipkan" ke dalam Client Component.

    Contoh nyata: komponen Laci (accordion) butuh state buka/tutup (client), tapi isinya data dari database (server). Solusinya: <Laci><StatistikDariDB /></Laci> ditulis di halaman server.

    Context provider

    React Context tidak jalan di Server Component. Buat provider sebagai Client Component yang menerima children, lalu pakai di layout server. Letakkan provider sedalam mungkin (bungkus children, bukan seluruh <html>). Inilah cara memasang QueryClientProvider dari TanStack Query di Next.js.

    Menjaga rahasia tetap di server

    Environment variable tanpa awalan NEXT_PUBLIC_ tidak pernah masuk bundle client (di client nilainya diganti string kosong). Untuk memastikan sebuah file tidak pernah di-import dari client, tambahkan import 'server-only' di atasnya; kalau ada Client Component yang meng-import-nya, build langsung error.

    Contoh kode

    Tombol interaktif sebagai Client Component kecil:

    // components/TombolSuka.jsx
    'use client' // batas: file ini dan import-nya dikirim ke browser
    
    import { useState } from 'react'
    
    export default function TombolSuka({ awal }) {
      const [suka, setSuka] = useState(awal)
      return (
        <button onClick={() => setSuka(suka + 1)} className="rounded border px-3 py-1">
          ❤️ {suka}
        </button>
      )
    }
    

    Halaman server yang mengambil data lalu meneruskan props:

    // app/blog/[slug]/page.jsx (bagian yang relevan)
    import { notFound } from 'next/navigation'
    import { getPost } from '@/lib/posts'
    import TombolSuka from '@/components/TombolSuka'
    
    export default async function PostPage({ params }) {
      const { slug } = await params
      const post = await getPost(slug) // ← jalan di server, kode query tidak dikirim ke browser
      if (!post) notFound()
    
      return (
        <article>
          <h1 className="text-3xl font-bold">{post.judul}</h1>
          <p>{post.isi}</p>
          <TombolSuka awal={0} /> {/* hanya tombol ini yang jadi JavaScript di browser */}
        </article>
      )
    }
    

    Composition pattern dengan children:

    // components/Laci.jsx
    'use client'
    
    import { useState } from 'react'
    
    export default function Laci({ judul, children }) {
      const [buka, setBuka] = useState(false)
      return (
        <div className="rounded border p-3">
          <button onClick={() => setBuka(!buka)}>{buka ? '▼' : '▶'} {judul}</button>
          {buka && <div className="mt-2">{children}</div>}
        </div>
      )
    }
    
    // app/(marketing)/tentang/page.jsx, Server Component
    import Laci from '@/components/Laci'
    import { getPosts } from '@/lib/posts'
    
    async function JumlahArtikel() {
      const posts = await getPosts() // query di server
      return <p>Kami sudah menulis {posts.length} artikel.</p>
    }
    
    export default function TentangPage() {
      return (
        <main>
          <h1 className="text-2xl font-bold">Tentang kami</h1>
          <Laci judul="Statistik">
            <JumlahArtikel /> {/* dirender di server, diselipkan ke Laci (client) */}
          </Laci>
        </main>
      )
    }
    // → klik "▶ Statistik" → "Kami sudah menulis 3 artikel."
    

    Provider untuk TanStack Query (dari materi sebelumnya) di Next.js:

    // app/providers.jsx
    'use client'
    
    import { useState } from 'react'
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    
    export default function Providers({ children }) {
      // useState agar QueryClient dibuat sekali per browser, bukan tiap render
      const [queryClient] = useState(() => new QueryClient())
      return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    }
    
    // app/layout.jsx, tetap Server Component
    import Providers from './providers'
    import './globals.css'
    
    export default function RootLayout({ children }) {
      return (
        <html lang="id">
          <body>
            <Providers>{children}</Providers>
          </body>
        </html>
      )
    }
    

    Mengunci file agar hanya bisa dipakai di server:

    // lib/rahasia.js
    import 'server-only' // build error kalau file ini di-import dari Client Component
    
    export async function getSaldo() {
      const res = await fetch('https://api.bank-contoh.id/saldo', {
        headers: { Authorization: `Bearer ${process.env.BANK_API_KEY}` }, // tanpa NEXT_PUBLIC_
      })
      return res.json()
    }
    

    Kesalahan umum

    1. Memakai useState/onClick tanpa 'use client'. Build gagal dengan pesan "You're importing a module that depends on useState into a React Server Component module. This API is only available in Client Components." Tambahkan directive di file komponen interaktifnya.
    2. Menaruh 'use client' di halaman paling atas. Seluruh halaman dan import-nya jadi JavaScript client, keuntungan server hilang. Pindahkan ke komponen kecil yang interaktif saja.
    3. Mengirim fungsi sebagai prop dari server ke client. Gagal: "Event handlers cannot be passed to Client Component props." Definisikan handler di dalam Client Component, atau gunakan Server Action ('use server').
    4. Meng-import Server Component dari Client Component. Komponen itu ikut berubah menjadi kode client (dan bisa error kalau memakai database). Kirim lewat children atau prop.
    5. Membaca process.env.RAHASIA di Client Component. Nilainya kosong di browser. Variabel yang memang boleh publik harus berawalan NEXT_PUBLIC_; rahasia hanya dibaca di server.

    Latihan

    1. Mudah, Buat Client Component Penghitung dengan tombol tambah dan kurang, lalu tampilkan di halaman server app/page.jsx yang juga menampilkan judul statis.
    2. Sedang, Buat Client Component Tab yang menerima dua prop berisi JSX (kiri dan kanan) dan menampilkan salah satunya sesuai tab aktif. Dari halaman server, kirim dua Server Component async (DaftarKopi, DaftarTeh) yang masing-masing mengembalikan daftar.
    3. Menantang, Buat ThemeProvider (Client Component dengan Context) yang menyimpan tema 'terang'/'gelap', pasang di root layout, lalu buat TombolTema (client) yang mengganti tema dan KartuProfil yang tetap Server Component.
    Lihat solusi

    1.

    // components/Penghitung.jsx
    'use client'
    
    import { useState } from 'react'
    
    export default function Penghitung() {
      const [n, setN] = useState(0)
      return (
        <div className="flex items-center gap-3">
          <button onClick={() => setN(n - 1)}>−</button>
          <span>{n}</span>
          <button onClick={() => setN(n + 1)}>+</button>
        </div>
      )
    }
    
    // app/page.jsx, Server Component
    import Penghitung from '@/components/Penghitung'
    
    export default function HomePage() {
      return (
        <main className="p-6">
          <h1 className="text-2xl font-bold">Penghitung Kopi</h1>
          <Penghitung />
        </main>
      )
    }
    

    2.

    // components/Tab.jsx
    'use client'
    
    import { useState } from 'react'
    
    export default function Tab({ kiri, kanan }) {
      const [aktif, setAktif] = useState('kiri')
      return (
        <div>
          <button onClick={() => setAktif('kiri')}>Kopi</button>
          <button onClick={() => setAktif('kanan')}>Teh</button>
          <div className="mt-3">{aktif === 'kiri' ? kiri : kanan}</div>
        </div>
      )
    }
    
    // app/minuman/page.jsx, Server Component
    import Tab from '@/components/Tab'
    
    async function DaftarKopi() {
      return <ul><li>Gayo</li><li>Toraja</li></ul>
    }
    
    async function DaftarTeh() {
      return <ul><li>Teh Tarik</li><li>Teh Melati</li></ul>
    }
    
    export default function MinumanPage() {
      return <Tab kiri={<DaftarKopi />} kanan={<DaftarTeh />} />
    }
    // → kedua daftar dirender di server; Tab hanya memilih mana yang ditampilkan
    

    3.

    // app/theme-provider.jsx
    'use client'
    
    import { createContext, useContext, useState } from 'react'
    
    const ThemeContext = createContext(null)
    
    export function useTema() {
      return useContext(ThemeContext)
    }
    
    export default function ThemeProvider({ children }) {
      const [tema, setTema] = useState('terang')
      return (
        <ThemeContext.Provider value={{ tema, setTema }}>
          <div className={tema === 'gelap' ? 'bg-gray-900 text-white' : 'bg-white text-black'}>
            {children}
          </div>
        </ThemeContext.Provider>
      )
    }
    
    // components/TombolTema.jsx
    'use client'
    
    import { useTema } from '@/app/theme-provider'
    
    export default function TombolTema() {
      const { tema, setTema } = useTema()
      return (
        <button onClick={() => setTema(tema === 'terang' ? 'gelap' : 'terang')}>
          Tema: {tema}
        </button>
      )
    }
    
    // components/KartuProfil.jsx, Server Component (tanpa 'use client')
    export default function KartuProfil() {
      return <div className="rounded border p-4">Barista: Dimas</div>
    }
    
    // app/layout.jsx
    import ThemeProvider from './theme-provider'
    import './globals.css'
    
    export default function RootLayout({ children }) {
      return (
        <html lang="id">
          <body>
            <ThemeProvider>{children}</ThemeProvider>
          </body>
        </html>
      )
    }
    
    // app/page.jsx
    import TombolTema from '@/components/TombolTema'
    import KartuProfil from '@/components/KartuProfil'
    
    export default function HomePage() {
      return (
        <main className="space-y-4 p-6">
          <TombolTema />
          <KartuProfil />
        </main>
      )
    }
    

    Ringkasan

    Video pendamping

    Kalau penjelasan di atas belum masuk, tonton ini. Video dari kanal YouTube pihak lain.

    When & Where to Add “use client” in React / Next.js (Client Components vs Server Components)ByteGrad · Bahasa InggrisBuka di YouTube

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.