kodingup.camp

Beranda/React Mastery: Styling, Pola Komponen, Data & Optimasi

Strategi Komposisi Komponen

Modul 5 dari 134 menit bacaComponent Composition Strategies

Isi modul

    Kenapa ini penting

    Kamu mungkin pernah melihat komponen dengan props seperti ini: <Card title="..." subtitle="..." showIcon iconType="star" footerText="..." showFooterButton footerButtonLabel="...">. Setiap kebutuhan baru menambah satu props lagi. Lama-lama komponennya penuh if dan sulit dirawat. React menyarankan cara lain: komposisi, yaitu menyusun komponen kecil di dalam komponen lain seperti menyusun kotak di dalam kotak. Komposisi juga merupakan obat pertama untuk masalah prop drilling (props dioper melewati banyak lapisan yang tidak membutuhkannya), sebelum kamu meraih Context.

    Konsep inti

    children: lubang serbaguna

    Setiap komponen otomatis menerima props spesial bernama children, yaitu apa pun yang kamu tulis di antara tag pembuka dan penutupnya. <Card><p>Halo</p></Card> berarti children milik Card adalah <p>Halo</p>. Komponen Card cukup menaruh {children} di tempat yang diinginkan. Card tidak perlu tahu isinya apa: teks, gambar, form, apa saja.

    Analogi: children itu seperti bingkai foto. Bingkai mengurus bentuk dan warna pinggiran, fotonya bebas diganti.

    Slot lewat props

    Kadang satu lubang tidak cukup. Halaman punya header, sidebar, dan konten. Di React, props bisa berisi JSX, jadi kamu bisa membuat beberapa "slot" bernama: <Layout header={<Navbar />} sidebar={<Menu />}>...</Layout>. Ini mirip konsep slot di Vue atau Web Components, tapi di React cukup memakai props biasa.

    Komposisi vs konfigurasi

    Konfigurasi (props data) Komposisi (props JSX/children)
    <Card title="Promo" icon="star" /> <Card><Card.Title>Promo</Card.Title>...</Card>
    Komponen menentukan tampilan isi Pemakai menentukan isi, komponen menentukan bingkai
    Cocok untuk komponen kecil yang seragam Cocok untuk komponen wadah yang isinya beragam
    Setiap variasi baru = props baru Variasi baru cukup susun isi berbeda

    Keduanya tidak saling meniadakan. Button dengan props varian (konfigurasi) itu wajar. Tapi untuk komponen wadah seperti Card, Modal, Layout, komposisi biasanya lebih tahan lama.

    Menghindari prop drilling dengan komposisi

    Prop drilling terjadi ketika App punya data user, lalu harus mengopernya ke Layout, lalu ke Header, lalu ke Avatar, padahal Layout dan Header tidak memakai user sama sekali. Mereka hanya "kurir".

    Solusi komposisi: biarkan App membuat <Avatar user={user} /> sendiri, lalu mengirimkannya sebagai JSX ke slot Layout. Layout cukup menaruh JSX itu, tanpa pernah tahu soal user. Rantai kurir terputus. Banyak kasus prop drilling selesai dengan cara ini tanpa Context sama sekali. Context baru dipakai kalau data dibutuhkan di banyak tempat yang tersebar (modul berikutnya).

    Komposisi di dunia nyata

    Hampir semua library komponen populer memakai komposisi. Komponen Card di banyak design system menyediakan potongan seperti CardHeader, CardContent, dan CardFooter yang kamu susun sendiri. Router menerima elemen halaman lewat props (element={<Beranda />}). Library layout menyediakan slot untuk sidebar dan header. Setelah memahami modul ini, kamu akan mengenali pola tersebut dan bisa memakainya dengan percaya diri.

    Seberapa banyak prop drilling yang masih wajar?

    Mengoper props satu sampai dua level adalah hal biasa dan justru membuat aliran data mudah dilacak: kamu bisa membaca kode dari atas ke bawah dan melihat persis dari mana data datang. Prop drilling baru menjadi masalah ketika: (1) banyak komponen perantara harus menerima props yang tidak mereka pakai, (2) setiap kali menambah data baru kamu harus mengubah lima file, atau (3) data yang sama dibutuhkan di banyak cabang yang berjauhan.

    Urutan solusinya: coba komposisi dulu (modul ini). Kalau data benar-benar dibutuhkan di banyak tempat yang tersebar, baru pakai Context (modul berikutnya). Urutan ini menjaga kode tetap eksplisit selama mungkin.

    Contoh kode

    Komponen Card dengan children dan Modal dengan beberapa slot:

    // Card.jsx
    export function Card({ children }) {
      return <div style={{ border: '1px solid #ddd', borderRadius: 12, padding: 16 }}>{children}</div>;
    }
    
    // Modal.jsx, beberapa slot: title, footer, dan children sebagai isi
    export function Modal({ title, footer, children, onClose }) {
      return (
        <div className="backdrop" onClick={onClose}>
          {/* stopPropagation supaya klik di dalam kotak tidak menutup modal */}
          <div className="modal" onClick={(e) => e.stopPropagation()}>
            <header>{title}</header>
            <section>{children}</section>
            {footer && <footer>{footer}</footer>}
          </div>
        </div>
      );
    }
    
    // Pemakaian Modal
    import { useState } from 'react';
    import { Modal } from './Modal.jsx';
    
    export default function HapusAkun() {
      const [buka, setBuka] = useState(false);
      return (
        <>
          <button onClick={() => setBuka(true)}>Hapus akun</button>
          {buka && (
            <Modal
              title={<h2>Yakin?</h2>}
              footer={
                <>
                  <button onClick={() => setBuka(false)}>Batal</button>
                  <button className="danger">Hapus</button>
                </>
              }
              onClose={() => setBuka(false)}
            >
              <p>Semua data kamu akan hilang permanen.</p>
            </Modal>
          )}
        </>
      );
    }
    

    Memutus prop drilling dengan slot:

    // SEBELUM: user dioper lewat Layout dan Header yang tidak memakainya
    function AppLama({ user }) {
      return <LayoutLama user={user} />;
    }
    function LayoutLama({ user }) {
      return <HeaderLama user={user} />;
    }
    function HeaderLama({ user }) {
      return <nav>Logo <Avatar user={user} /></nav>;
    }
    
    // SESUDAH: App merakit Avatar sendiri, Layout & Header hanya menerima JSX
    function Avatar({ user }) {
      return <img src={user.foto} alt={user.nama} width={32} />;
    }
    
    function Header({ kanan }) {
      return <nav>Logo {kanan}</nav>;
    }
    
    function Layout({ headerKanan, children }) {
      return (
        <>
          <Header kanan={headerKanan} />
          <main>{children}</main>
        </>
      );
    }
    
    export default function App() {
      const user = { nama: 'Sari', foto: '/sari.jpg' };
      return (
        <Layout headerKanan={<Avatar user={user} />}>
          <p>Selamat datang, {user.nama}!</p>
        </Layout>
      );
    }
    // Layout dan Header tidak lagi punya props user sama sekali
    

    Kesalahan umum

    1. Lupa merender {children}. Komponen menerima isi tapi tidak menaruhnya di JSX, sehingga isinya tidak tampil. Pastikan {children} ada di return.
    2. Menambah props boolean untuk setiap variasi. showFooter, showIcon, showBadge adalah tanda komponen perlu diubah ke komposisi.
    3. Langsung memakai Context untuk prop drilling dua-tiga level. Context membuat aliran data lebih tersembunyi. Coba dulu komposisi, atau terima saja oper props dua level karena itu masih wajar.
    4. Mengirim komponen, bukan elemen. header={Navbar} mengirim fungsinya, bukan hasil render. Yang benar header={<Navbar />}, kecuali komponen penerima memang dirancang menerima tipe komponen.
    5. Memodifikasi children dengan cara rumit. Menggunakan React.Children.map atau cloneElement untuk menyuntik props ke children sulit dirawat. Lebih baik pakai Context (seperti compound component) atau slot eksplisit.

    Latihan

    1. Mudah, Buat komponen Panel yang menerima judul (string) dan children, lalu tampilkan judul tebal di atas isinya.
    2. Sedang, Buat komponen PageLayout dengan slot sidebar, header, dan children. Gunakan untuk halaman dashboard sederhana.
    3. Menantang, Ada rantai App → Dashboard → Toolbar → ThemeToggle di mana tema dan setTema dioper sampai ke ThemeToggle. Refactor dengan komposisi agar Dashboard dan Toolbar tidak menerima props tema.
    Lihat solusi
    // 1.
    export function Panel({ judul, children }) {
      return (
        <section>
          <strong>{judul}</strong>
          <div>{children}</div>
        </section>
      );
    }
    // <Panel judul="Statistik"><p>100 pengunjung</p></Panel>
    
    // 2.
    export function PageLayout({ header, sidebar, children }) {
      return (
        <div style={{ display: 'grid', gridTemplateColumns: '200px 1fr' }}>
          <header style={{ gridColumn: '1 / 3' }}>{header}</header>
          <aside>{sidebar}</aside>
          <main>{children}</main>
        </div>
      );
    }
    
    export default function Dashboard() {
      return (
        <PageLayout header={<h1>Dashboard</h1>} sidebar={<nav>Menu</nav>}>
          <p>Ringkasan penjualan hari ini.</p>
        </PageLayout>
      );
    }
    
    // 3.
    import { useState } from 'react';
    
    function ThemeToggle({ tema, setTema }) {
      return <button onClick={() => setTema(tema === 'terang' ? 'gelap' : 'terang')}>Tema: {tema}</button>;
    }
    
    function Toolbar({ aksi }) {
      return <div className="toolbar">{aksi}</div>;
    }
    
    function Dashboard({ toolbarAksi }) {
      return (
        <>
          <Toolbar aksi={toolbarAksi} />
          <p>Isi dashboard</p>
        </>
      );
    }
    
    export default function App() {
      const [tema, setTema] = useState('terang');
      return <Dashboard toolbarAksi={<ThemeToggle tema={tema} setTema={setTema} />} />;
    }
    

    Ringkasan

    Kuis pemahaman

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