nextjsrscpatternMahir4 mnt baca

Pola Komposisi Server-Client

Menyisipkan Server Component ke dalam Client Component via children.

Masalah: client dilarang mengimpor server

Aturannya tegas: Client Component tidak boleh mengimpor Server Component secara langsung. Kenapa aturannya sekeras itu? Karena import berarti kode server ikut terbundel menjadi JavaScript client, dan rahasia server (query database, API key) bisa bocor ke browser. Next.js menolak pola ini dengan error saat build, dan itu melindungimu.

Tapi kebutuhan nyatanya sering muncul: sebuah UI tab yang interaktif (client, butuh useState) ingin menampilkan daftar yang datanya di-fetch dari database (server). Kalau import langsung dilarang, bagaimana caranya?

Solusi: selipkan lewat children atau slot

Triknya: jangan biarkan Client Component yang mengimpor Server Component. Biarkan parent yang server me-render keduanya, lalu selipkan Server Component sebagai children atau props bernama (slot). Client Component hanya menerima hasil render-nya, bukan kodenya.

Kenapa ini aman? Children di-render di server terlebih dahulu menjadi payload React, baru dikirim ke browser. Client Component tidak pernah melihat query database atau secret di baliknya, ia hanya menampilkan HTML yang sudah jadi dan mengatur interaktivitas di sekitarnya.

Contoh 1: tab interaktif berisi konten server

tsx
// app/dashboard/page.tsx (Server Component, si "mak comblang")
import ClientTabs from "./ClientTabs";
import ProfileServer from "./ProfileServer";
import StatsServer from "./StatsServer";

export default function DashboardPage() {
  return (
    <ClientTabs
      tabs={[
        { id: "profil", label: "Profil", content: <ProfileServer /> },
        { id: "statistik", label: "Statistik", content: <StatsServer /> },
      ]}
    />
  );
}
tsx
// app/dashboard/ClientTabs.tsx
"use client";

import { useState, type ReactNode } from "react";

type Tab = { id: string; label: string; content: ReactNode };

export default function ClientTabs({ tabs }: { tabs: Tab[] }) {
  const [active, setActive] = useState(tabs[0].id);
  const current = tabs.find((t) => t.id === active);

  return (
    <div>
      <div role="tablist">
        {tabs.map((t) => (
          <button
            key={t.id}
            role="tab"
            aria-selected={t.id === active}
            onClick={() => setActive(t.id)}
          >
            {t.label}
          </button>
        ))}
      </div>
      <div role="tabpanel">{current?.content}</div>
    </div>
  );
}

ProfileServer dan StatsServer tetap murni server (boleh query database), ClientTabs murni client (mengatur tab aktif). Keduanya tidak pernah saling mengimpor, page server yang mempertemukan mereka.

Contoh 2: layout toko dengan banyak slot

Pola slot juga bekerja untuk beberapa area sekaligus lewat props bernama:

tsx
// app/shop/ShopShell.tsx
"use client";

import { useState, type ReactNode } from "react";

export default function ShopShell({
  sidebar,
  banner,
  children,
}: {
  sidebar: ReactNode;
  banner: ReactNode;
  children: ReactNode;
}) {
  const [collapsed, setCollapsed] = useState(false);

  return (
    <div className={collapsed ? "shell collapsed" : "shell"}>
      <button onClick={() => setCollapsed(!collapsed)}>
        {collapsed ? "Buka" : "Tutup"} sidebar
      </button>
      {banner}
      <div style={{ display: "flex" }}>
        {!collapsed && <aside>{sidebar}</aside>}
        <main>{children}</main>
      </div>
    </div>
  );
}
tsx
// app/shop/layout.tsx (server, mengisi slot dengan Server Component)
import ShopShell from "./ShopShell";
import CategoryNav from "./CategoryNav"; // server: fetch kategori dari DB
import PromoBanner from "./PromoBanner"; // server: fetch promo aktif

export default function ShopLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <ShopShell sidebar={<CategoryNav />} banner={<PromoBanner />}>
      {children}
    </ShopShell>
  );
}

Sidebar kategori dan banner promo di-fetch di server, kerangka interaktifnya (collapse/expand) hidup di client. Satu file client ramping mengatur banyak konten server.

Kapan pola ini dipakai

Pakai komposisi slot setiap kali kamu tergoda menulis import DataList from "./DataList" di dalam file "use client". Itu sinyal bahwa DataList harusnya disisipkan dari parent server, bukan diimpor langsung. Ingat arah yang diizinkan hanya satu: Server Component boleh mengimpor Client Component, tidak sebaliknya. Pola slot adalah jembatan resmi untuk kebutuhan yang terlihat seperti "sebaliknya" itu.

Tantangan

Tabs komposisi

Buat ClientTabs (client) yang menerima dua Server Component (ProfilServer, StatsServer) sebagai children untuk dua tab. Pastikan tidak ada import server-di-client langsung.