Bab 5: Passing Props ke Komponen

5 menit baca

Props: Cara Komponen Ngobrol

Bayangin kamu di warung nasi padang. Kamu bilang ke abangnya: "Bang, nasi rendang, sayur nangka, telor balado." Abangnya dengerin pesananmu, terus nyiapin piring sesuai permintaan. Kamu kasih informasi ke abangnya, dan dia bertindak berdasarkan informasi itu.

Props di React persis kayak gitu. Props adalah cara kamu mengirim informasi dari komponen induk (parent) ke komponen anak (child). Komponen anak menerima informasi itu dan menampilkan dirinya sesuai informasi yang diterima.

jsx
// Parent "memesan" ke child
function App() {
  return (
    <KartuProfil 
      nama="Siti Rahayu" 
      pekerjaan="Data Scientist" 
      kota="Yogyakarta" 
    />
  );
}

// Child "menyajikan" sesuai pesanan
function KartuProfil(props) {
  return (
    <div className="kartu">
      <h2>{props.nama}</h2>
      <p>{props.pekerjaan}</p>
      <p>📍 {props.kota}</p>
    </div>
  );
}

Coba sendiri: Edit kode di bawah dan lihat hasilnya langsung!

Kenapa Props Penting?

Tanpa props, setiap komponen akan statis dan nggak bisa dipakai ulang. Bayangin kalau setiap pelanggan di warung harus punya warung sendiri karena warungnya cuma bisa bikin satu menu. Nggak efisien kan?

Props bikin komponen jadi reusable (bisa dipakai ulang):

jsx
// TANPA props - harus bikin komponen baru untuk setiap orang 😩
function KartuBudi() {
  return <div><h2>Budi</h2><p>Jakarta</p></div>;
}
function KartuSiti() {
  return <div><h2>Siti</h2><p>Bandung</p></div>;
}
function KartuAhmad() {
  return <div><h2>Ahmad</h2><p>Surabaya</p></div>;
}

// DENGAN props - satu komponen untuk semua! 🎉
function KartuProfil({ nama, kota }) {
  return <div><h2>{nama}</h2><p>{kota}</p></div>;
}

function App() {
  return (
    <>
      <KartuProfil nama="Budi" kota="Jakarta" />
      <KartuProfil nama="Siti" kota="Bandung" />
      <KartuProfil nama="Ahmad" kota="Surabaya" />
    </>
  );
}

Cara Mengirim Props

Props dikirim lewat atribut di JSX, mirip kayak atribut HTML:

jsx
function App() {
  return (
    <Produk 
      nama="Sepatu Sneakers"       // String
      harga={450000}               // Number (pakai kurung kurawal)
      tersedia={true}              // Boolean
      tags={["casual", "sport"]}   // Array
      detail={{                    // Object (kurung kurawal ganda!)
        warna: "hitam",
        ukuran: 42
      }}
      onBeli={() => alert("Dibeli!")}  // Function
    />
  );
}

Aturan tipe data:

  • String: boleh pakai kutip langsung nama="Budi" atau kurung kurawal nama={"Budi"}
  • Selain string (number, boolean, array, object, function): harus pakai kurung kurawal
jsx
// Khusus boolean true, ada shorthand:
<Input required={true} />
// Sama dengan:
<Input required />

// Tapi kalau false, harus eksplisit:
<Input required={false} />

Cara Menerima Props

Cara 1: Parameter props (Objek Utuh)

jsx
function Sapaan(props) {
  return (
    <div>
      <h1>Halo, {props.nama}!</h1>
      <p>Umur: {props.umur} tahun</p>
      <p>Kota: {props.kota}</p>
    </div>
  );
}

Semua props yang dikirim parent masuk sebagai satu objek. Kalau parent kirim nama="Budi" umur={25} kota="Jakarta", maka props isinya { nama: "Budi", umur: 25, kota: "Jakarta" }.

Cara 2: Destructuring (Lebih Populer!)

jsx
// Destructuring langsung di parameter
function Sapaan({ nama, umur, kota }) {
  return (
    <div>
      <h1>Halo, {nama}!</h1>
      <p>Umur: {umur} tahun</p>
      <p>Kota: {kota}</p>
    </div>
  );
}

Destructuring lebih populer karena:

  1. Langsung keliatan props apa aja yang dipakai komponen ini
  2. Nggak perlu nulis props. berulang-ulang
  3. Kode lebih bersih dan mudah dibaca
jsx
// Destructuring dengan rename (jarang, tapi bisa)
function Komponen({ nama: namaLengkap, umur: usia }) {
  return <p>{namaLengkap}, {usia} tahun</p>;
}

Default Values (Nilai Bawaan)

Kadang props itu opsional. Kalau nggak dikirim, kamu mau ada nilai default-nya. Kayak di warung: "Kalau nggak bilang level pedas, default-nya sedang."

jsx
// Cara 1: Default di destructuring (RECOMMENDED)
function Tombol({ teks = "Klik Saya", warna = "blue", ukuran = "medium" }) {
  return (
    <button style={{ 
      backgroundColor: warna,
      padding: ukuran === 'large' ? '16px 32px' : '8px 16px',
      color: 'white',
      border: 'none',
      borderRadius: '4px'
    }}>
      {teks}
    </button>
  );
}

// Penggunaan:
<Tombol />                          // Pakai semua default
<Tombol teks="Simpan" />            // Override teks, sisanya default
<Tombol teks="Hapus" warna="red" /> // Override teks dan warna
<Tombol teks="Submit" warna="green" ukuran="large" />  // Override semua
jsx
// Cara 2: Default pakai || atau ?? (kurang recommended tapi sering ditemui)
function Avatar({ url, nama }) {
  const fotoUrl = url || "https://example.com/default-avatar.png";
  const altText = nama ?? "Pengguna";
  
  return <img src={fotoUrl} alt={altText} />;
}

Perbedaan || vs ??:

  • || menganggap 0, "", false sebagai "kosong" (fallback ke default)
  • ?? cuma fallback kalau nilainya null atau undefined
jsx
// Contoh perbedaan:
function Skor({ nilai = 0 }) {
  // Kalau parent kirim nilai={0}, ini tetap 0 ✅
  return <p>Skor: {nilai}</p>;
}

function SkorBuggy({ nilai }) {
  const skor = nilai || 100;  // ❌ Bug! Kalau nilai=0, jadi 100
  return <p>Skor: {skor}</p>;
}

Spreading Props

Kadang kamu punya objek yang isinya persis props yang mau dikirim. Daripada nulis satu-satu, bisa pakai spread operator ...:

jsx
function App() {
  const dataProfil = {
    nama: "Dewi Lestari",
    umur: 45,
    pekerjaan: "Penulis",
    kota: "Bandung",
    foto: "dewi.jpg"
  };
  
  // ❌ Cara panjang - nulis satu-satu
  return (
    <Profil 
      nama={dataProfil.nama}
      umur={dataProfil.umur}
      pekerjaan={dataProfil.pekerjaan}
      kota={dataProfil.kota}
      foto={dataProfil.foto}
    />
  );
  
  // ✅ Cara singkat - spread!
  return <Profil {...dataProfil} />;
}

Kapan pakai spread?

  • Ketika kamu mau forward semua props ke komponen anak
  • Ketika data udah dalam bentuk objek yang cocok

Kapan JANGAN pakai spread?

  • Ketika kamu nggak tahu isi objeknya (bisa kirim props yang nggak dibutuhkan)
  • Ketika kamu mau eksplisit soal props apa yang dikirim (lebih readable)
jsx
// Pattern umum: spread + override
function App() {
  const defaultConfig = {
    warna: "blue",
    ukuran: "medium",
    rounded: true
  };
  
  // Spread default, tapi override warna
  return <Tombol {...defaultConfig} warna="red" />;
  // Hasilnya: warna="red", ukuran="medium", rounded={true}
}
jsx
// Pattern: forwarding props
function WrapperKartu({ className, ...sisaProps }) {
  // Ambil className untuk wrapper, sisanya forward ke child
  return (
    <div className={`wrapper ${className}`}>
      <KartuDalam {...sisaProps} />
    </div>
  );
}

Children: Props Spesial

Ada satu prop yang spesial banget: children. Ini adalah apapun yang kamu taruh di antara tag pembuka dan penutup komponen.

jsx
// Ini:
<Tombol>Klik Saya</Tombol>

// Sama dengan ini:
<Tombol children="Klik Saya" />

children bikin komponen jadi kayak "wadah" yang bisa diisi apapun:

jsx
// Komponen Card yang bisa diisi apapun
function Card({ judul, children }) {
  return (
    <div className="card">
      <div className="card-header">
        <h3>{judul}</h3>
      </div>
      <div className="card-body">
        {children}  {/* Apapun yang ditaruh di antara <Card>...</Card> */}
      </div>
    </div>
  );
}

// Penggunaan - isi bisa beda-beda!
function App() {
  return (
    <>
      <Card judul="Profil Saya">
        <p>Nama: Budi Santoso</p>
        <p>Kota: Jakarta</p>
        <img src="budi.jpg" alt="Foto Budi" />
      </Card>
      
      <Card judul="Statistik">
        <ul>
          <li>Pengikut: 1.250</li>
          <li>Mengikuti: 340</li>
          <li>Postingan: 89</li>
        </ul>
      </Card>
      
      <Card judul="Aksi">
        <button>Edit Profil</button>
        <button>Logout</button>
      </Card>
    </>
  );
}
💡Info

Bayangin children itu kayak bingkai foto. Bingkainya (komponen Card) selalu sama: ada border, ada shadow, ada header. Tapi foto yang dipajang di dalamnya (children) bisa beda-beda. Satu bingkai bisa dipake buat foto keluarga, foto pemandangan, atau foto kucing.

Children Bisa Apa Aja

jsx
// Children bisa teks biasa
<Tombol>Simpan</Tombol>

// Children bisa elemen JSX
<Modal>
  <h2>Konfirmasi</h2>
  <p>Yakin mau hapus?</p>
</Modal>

// Children bisa komponen lain
<Layout>
  <Navbar />
  <Sidebar />
  <Konten />
</Layout>

// Children bahkan bisa kosong (self-closing)
<Divider />

Contoh Nyata: Layout Pattern

jsx
function Layout({ children }) {
  return (
    <div className="layout">
      <header className="navbar">
        <h1>Toko Online</h1>
        <nav>Beranda | Produk | Keranjang</nav>
      </header>
      
      <main className="konten">
        {children}  {/* Halaman yang berbeda-beda */}
      </main>
      
      <footer className="footer">
        <p>© 2024 Toko Online. Semua hak dilindungi.</p>
      </footer>
    </div>
  );
}

// Halaman Beranda
function Beranda() {
  return (
    <Layout>
      <h2>Selamat Datang!</h2>
      <p>Temukan produk terbaik di sini.</p>
    </Layout>
  );
}

// Halaman Produk
function HalamanProduk() {
  return (
    <Layout>
      <h2>Daftar Produk</h2>
      <KartuProduk nama="Sepatu" harga={300000} />
      <KartuProduk nama="Tas" harga={250000} />
    </Layout>
  );
}

Props Adalah Read-Only (Immutable)

Ini aturan paling penting soal props: komponen TIDAK BOLEH mengubah props yang diterimanya.

Analoginya: Kalau kamu pinjam buku dari perpustakaan, kamu boleh baca tapi nggak boleh coret-coret. Props itu "pinjaman" dari parent, bukan milik child.

jsx
// ❌ SALAH - jangan pernah ubah props!
function Sapaan({ nama }) {
  nama = nama.toUpperCase();  // JANGAN! Ini mengubah props
  return <h1>Halo, {nama}</h1>;
}

// ✅ BENAR - bikin variabel baru
function Sapaan({ nama }) {
  const namaKapital = nama.toUpperCase();  // Bikin variabel baru, props nggak diubah
  return <h1>Halo, {namaKapital}</h1>;
}
jsx
// ❌ SALAH - mutasi objek props
function DaftarItem({ items }) {
  items.push("Item baru");  // JANGAN! Ini mengubah array dari parent
  return <ul>{items.map(item => <li key={item}>{item}</li>)}</ul>;
}

// ✅ BENAR - bikin array baru
function DaftarItem({ items }) {
  const semuaItems = [...items, "Item baru"];  // Array baru, yang lama nggak diubah
  return <ul>{semuaItems.map(item => <li key={item}>{item}</li>)}</ul>;
}

Kenapa props harus read-only?

  1. Predictability: Kalau props bisa diubah child, parent nggak bisa prediksi apa yang terjadi. Kayak kamu nitip uang ke temen, terus temen kamu belanjain tanpa izin.

  2. One-way data flow: Data di React mengalir satu arah: dari atas ke bawah (parent ke child). Ini bikin aplikasi lebih mudah di-debug.

  3. Re-render yang benar: React perlu tahu kapan harus update tampilan. Kalau props diubah diam-diam, React nggak tahu dan tampilan bisa nggak sinkron.

Props vs State: Apa Bedanya?

Ini pertanyaan yang sering muncul. Singkatnya:

PropsState
Siapa yang "punya"?ParentKomponen itu sendiri
Bisa diubah?❌ Tidak (read-only)✅ Ya (pakai setState)
Dari mana?Dikirim dari luarDibuat di dalam komponen
AnaloginyaPesanan dari pelangganCatatan internal dapur
jsx
// Props = data dari luar (parent kirim)
// State = data internal (komponen kelola sendiri)

function Keranjang({ maxItem }) {  // maxItem = props (dari parent)
  const [jumlahItem, setJumlahItem] = useState(0);  // jumlahItem = state (internal)
  
  return (
    <div>
      <p>Item di keranjang: {jumlahItem} / {maxItem}</p>
      <button onClick={() => {
        if (jumlahItem < maxItem) {
          setJumlahItem(jumlahItem + 1);  // ✅ State boleh diubah
        }
      }}>
        Tambah Item
      </button>
    </div>
  );
}

// Parent menentukan batas (props), child mengelola jumlah (state)
<Keranjang maxItem={10} />

Kita akan bahas state lebih detail di Part 3. Untuk sekarang, ingat aja: props = input dari luar, state = data internal.

Pola-Pola Props yang Sering Dipakai

Pattern 1: Render Berbeda Berdasarkan Props

jsx
function Alert({ tipe = "info", pesan }) {
  const warna = {
    info: { bg: '#e3f2fd', border: '#2196f3', icon: 'ℹ️' },
    sukses: { bg: '#e8f5e9', border: '#4caf50', icon: '✅' },
    warning: { bg: '#fff3e0', border: '#ff9800', icon: '⚠️' },
    error: { bg: '#ffebee', border: '#f44336', icon: '❌' },
  };
  
  const style = warna[tipe] || warna.info;
  
  return (
    <div style={{
      padding: '12px 16px',
      backgroundColor: style.bg,
      borderLeft: `4px solid ${style.border}`,
      borderRadius: '4px',
      margin: '8px 0'
    }}>
      <span>{style.icon}</span> {pesan}
    </div>
  );
}

// Penggunaan
<Alert tipe="sukses" pesan="Data berhasil disimpan!" />
<Alert tipe="error" pesan="Gagal menghubungi server." />
<Alert tipe="warning" pesan="Stok tinggal 3 item." />
<Alert pesan="Ini alert info default." />

Pattern 2: Komponen Komposisi

jsx
// Komponen kecil yang bisa dikombinasikan
function Avatar({ src, alt, ukuran = 40 }) {
  return (
    <img 
      src={src} 
      alt={alt}
      style={{ 
        width: ukuran, 
        height: ukuran, 
        borderRadius: '50%' 
      }}
    />
  );
}

function NamaPengguna({ nama, verified = false }) {
  return (
    <span>
      <strong>{nama}</strong>
      {verified && ' ✓'}
    </span>
  );
}

function InfoPengguna({ user }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
      <Avatar src={user.foto} alt={user.nama} ukuran={48} />
      <div>
        <NamaPengguna nama={user.nama} verified={user.verified} />
        <p style={{ margin: 0, color: '#666' }}>{user.bio}</p>
      </div>
    </div>
  );
}

Pattern 3: Callback Props (Fungsi sebagai Props)

jsx
// Parent kirim fungsi, child panggil fungsi itu
function App() {
  function handleBeli(namaProduk) {
    alert(`${namaProduk} ditambahkan ke keranjang!`);
  }
  
  return (
    <div>
      <Produk nama="Nasi Goreng" harga={15000} onBeli={handleBeli} />
      <Produk nama="Mie Ayam" harga={12000} onBeli={handleBeli} />
    </div>
  );
}

function Produk({ nama, harga, onBeli }) {
  return (
    <div className="produk">
      <h3>{nama}</h3>
      <p>Rp {harga.toLocaleString('id-ID')}</p>
      {/* Child memanggil fungsi dari parent */}
      <button onClick={() => onBeli(nama)}>
        Beli
      </button>
    </div>
  );
}

Ini pola penting! Data mengalir ke bawah (parent → child) lewat props. Tapi kalau child mau "ngomong" ke parent, dia panggil fungsi callback yang dikirim parent lewat props.

Contoh Lengkap: Sistem Kartu Menu Restoran

jsx
// Komponen Badge
function Badge({ teks, warna = "#666" }) {
  return (
    <span style={{
      backgroundColor: warna,
      color: 'white',
      padding: '2px 8px',
      borderRadius: '12px',
      fontSize: '0.75rem',
      marginLeft: '8px'
    }}>
      {teks}
    </span>
  );
}

// Komponen Rating Bintang
function Rating({ nilai, maxBintang = 5 }) {
  const bintangPenuh = Math.floor(nilai);
  const sisaBintang = maxBintang - bintangPenuh;
  
  return (
    <span>
      {'⭐'.repeat(bintangPenuh)}
      {'☆'.repeat(sisaBintang)}
      <span style={{ marginLeft: '4px', color: '#666' }}>({nilai})</span>
    </span>
  );
}

// Komponen Kartu Menu
function KartuMenu({ 
  nama, 
  harga, 
  deskripsi, 
  rating = 0, 
  gambar,
  kategori,
  bestseller = false,
  habis = false,
  onPesan 
}) {
  return (
    <div style={{
      border: '1px solid #ddd',
      borderRadius: '12px',
      overflow: 'hidden',
      opacity: habis ? 0.6 : 1,
      maxWidth: '300px'
    }}>
      {/* Gambar */}
      <img 
        src={gambar} 
        alt={nama}
        style={{ width: '100%', height: '180px', objectFit: 'cover' }}
      />
      
      {/* Konten */}
      <div style={{ padding: '16px' }}>
        <div style={{ display: 'flex', alignItems: 'center' }}>
          <h3 style={{ margin: 0 }}>{nama}</h3>
          {bestseller && <Badge teks="Best Seller" warna="#e91e63" />}
          {habis && <Badge teks="Habis" warna="#9e9e9e" />}
        </div>
        
        <p style={{ color: '#666', fontSize: '0.9rem' }}>{deskripsi}</p>
        
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          <span style={{ fontWeight: 'bold', fontSize: '1.1rem' }}>
            Rp {harga.toLocaleString('id-ID')}
          </span>
          <Rating nilai={rating} />
        </div>
        
        <p style={{ color: '#888', fontSize: '0.8rem' }}>Kategori: {kategori}</p>
        
        <button 
          onClick={() => onPesan(nama)}
          disabled={habis}
          style={{
            width: '100%',
            padding: '10px',
            backgroundColor: habis ? '#ccc' : '#4caf50',
            color: 'white',
            border: 'none',
            borderRadius: '6px',
            cursor: habis ? 'not-allowed' : 'pointer',
            marginTop: '8px'
          }}
        >
          {habis ? 'Stok Habis' : 'Pesan Sekarang'}
        </button>
      </div>
    </div>
  );
}

// Penggunaan
function MenuRestoran() {
  function handlePesan(namaMenu) {
    alert(`${namaMenu} ditambahkan ke pesanan!`);
  }
  
  return (
    <div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
      <KartuMenu 
        nama="Nasi Goreng Spesial"
        harga={25000}
        deskripsi="Nasi goreng dengan telur, ayam, dan sayuran segar"
        rating={4.8}
        gambar="nasi-goreng.jpg"
        kategori="Makanan Utama"
        bestseller={true}
        onPesan={handlePesan}
      />
      <KartuMenu 
        nama="Es Teh Tarik"
        harga={12000}
        deskripsi="Teh tarik khas dengan foam lembut"
        rating={4.5}
        gambar="teh-tarik.jpg"
        kategori="Minuman"
        onPesan={handlePesan}
      />
      <KartuMenu 
        nama="Soto Betawi"
        harga={30000}
        deskripsi="Soto santan khas Jakarta dengan daging sapi"
        rating={4.2}
        gambar="soto-betawi.jpg"
        kategori="Makanan Utama"
        habis={true}
        onPesan={handlePesan}
      />
    </div>
  );
}

⚠️ Jebakan

Jebakan 1: Lupa Kurung Kurawal untuk Non-String

jsx
// ❌ Ini kirim STRING "42", bukan NUMBER 42
<Komponen umur="42" />

// ✅ Ini kirim NUMBER 42
<Komponen umur={42} />

// ❌ Ini kirim STRING "true", bukan BOOLEAN true
<Komponen aktif="true" />

// ✅ Ini kirim BOOLEAN true
<Komponen aktif={true} />
// Atau shorthand:
<Komponen aktif />

Jebakan 2: Mutasi Props

jsx
// ❌ JANGAN ubah props!
function Daftar({ items }) {
  items.sort();  // Ini MENGUBAH array asli dari parent!
  return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>;
}

// ✅ Bikin copy dulu
function Daftar({ items }) {
  const sortedItems = [...items].sort();  // Copy baru, aman
  return <ul>{sortedItems.map(i => <li key={i}>{i}</li>)}</ul>;
}

Jebakan 3: Spread yang Kebablasan

jsx
// ❌ Spread objek yang nggak kamu kontrol - bisa kirim props aneh
function App() {
  const dataUser = await fetchUser();  // Entah isinya apa
  return <Profil {...dataUser} />;     // Bisa kirim props yang nggak dibutuhkan
}

// ✅ Lebih aman - eksplisit
function App() {
  const dataUser = await fetchUser();
  return (
    <Profil 
      nama={dataUser.nama}
      email={dataUser.email}
      foto={dataUser.foto}
    />
  );
}

Jebakan 4: Children vs Prop Biasa

jsx
// Dua cara ini BERBEDA:

// Cara 1: children lewat konten
<Card>
  <p>Ini children</p>
</Card>

// Cara 2: children lewat prop eksplisit
<Card children={<p>Ini juga children</p>} />

// Kalau dua-duanya ada, yang di konten MENANG:
<Card children={<p>Ini kalah</p>}>
  <p>Ini yang ditampilkan</p>
</Card>

Jebakan 5: Props Drilling (Masalah di Aplikasi Besar)

jsx
// ❌ Props drilling - kirim props melewati banyak level
function App() {
  const user = { nama: "Budi" };
  return <Layout user={user} />;
}
function Layout({ user }) {
  return <Sidebar user={user} />;  // Layout nggak pakai user, cuma forward
}
function Sidebar({ user }) {
  return <Avatar user={user} />;   // Sidebar juga cuma forward
}
function Avatar({ user }) {
  return <img alt={user.nama} />;  // Baru di sini dipake
}

// Solusinya nanti: Context API (dibahas di bab lain)

Ringkasan

KonsepPenjelasan
PropsData yang dikirim parent ke child
Destructuring{ nama, umur } langsung di parameter
Default values{ nama = "Anonim" } untuk nilai bawaan
Spread{...objek} untuk forward semua props
ChildrenKonten di antara tag pembuka dan penutup
Read-onlyProps TIDAK BOLEH diubah oleh child
Callback propsFungsi yang dikirim parent, dipanggil child

🏋️ Challenge

Challenge 1: Komponen Tombol Fleksibel

Buat komponen Tombol yang menerima props:

  • teks (default: "Klik")
  • varian ("primary", "secondary", "danger") yang mengubah warna
  • ukuran ("sm", "md", "lg") yang mengubah padding dan font-size
  • disabled (boolean)
  • onClick (fungsi callback)
💡 Hint
  • Bikin objek mapping untuk warna berdasarkan varian
  • Bikin objek mapping untuk ukuran (padding + fontSize)
  • Gabungkan semuanya di atribut style
  • Jangan lupa handle disabled di style (opacity) dan di atribut button
✅ Solusi
jsx
function Tombol({ 
  teks = "Klik", 
  varian = "primary", 
  ukuran = "md", 
  disabled = false, 
  onClick 
}) {
  const warnaMap = {
    primary: { bg: '#1976d2', hover: '#1565c0' },
    secondary: { bg: '#757575', hover: '#616161' },
    danger: { bg: '#d32f2f', hover: '#c62828' },
  };
  
  const ukuranMap = {
    sm: { padding: '6px 12px', fontSize: '0.8rem' },
    md: { padding: '10px 20px', fontSize: '1rem' },
    lg: { padding: '14px 28px', fontSize: '1.2rem' },
  };
  
  const warna = warnaMap[varian] || warnaMap.primary;
  const size = ukuranMap[ukuran] || ukuranMap.md;
  
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      style={{
        backgroundColor: warna.bg,
        color: 'white',
        padding: size.padding,
        fontSize: size.fontSize,
        border: 'none',
        borderRadius: '6px',
        cursor: disabled ? 'not-allowed' : 'pointer',
        opacity: disabled ? 0.5 : 1,
      }}
    >
      {teks}
    </button>
  );
}

// Penggunaan:
function App() {
  return (
    <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
      <Tombol teks="Simpan" varian="primary" ukuran="lg" onClick={() => alert('Disimpan!')} />
      <Tombol teks="Batal" varian="secondary" />
      <Tombol teks="Hapus" varian="danger" ukuran="sm" />
      <Tombol teks="Disabled" disabled={true} />
    </div>
  );
}

Challenge 2: Kartu Testimoni dengan Children

Buat komponen KartuTestimoni yang:

  • Menerima props: nama, jabatan, foto, rating (1-5)
  • Menggunakan children untuk isi testimoni (bisa teks apapun)
  • Menampilkan bintang sesuai rating
  • Punya border warna emas kalau rating 5
💡 Hint
  • children otomatis tersedia sebagai prop
  • '⭐'.repeat(rating) untuk bintang
  • Conditional style: border: rating === 5 ? '2px solid gold' : '1px solid #ddd'
✅ Solusi
jsx
function KartuTestimoni({ nama, jabatan, foto, rating = 5, children }) {
  return (
    <div style={{
      padding: '20px',
      borderRadius: '12px',
      border: rating === 5 ? '2px solid gold' : '1px solid #ddd',
      backgroundColor: rating === 5 ? '#fffde7' : 'white',
      maxWidth: '400px',
      margin: '12px 0'
    }}>
      {/* Rating */}
      <div style={{ marginBottom: '12px' }}>
        {'⭐'.repeat(rating)}{'☆'.repeat(5 - rating)}
      </div>
      
      {/* Isi testimoni (children) */}
      <blockquote style={{ 
        fontStyle: 'italic', 
        margin: '0 0 16px 0',
        color: '#444',
        lineHeight: '1.6'
      }}>
        "{children}"
      </blockquote>
      
      {/* Info pengguna */}
      <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
        <img 
          src={foto} 
          alt={nama}
          style={{ width: 40, height: 40, borderRadius: '50%' }}
        />
        <div>
          <strong>{nama}</strong>
          <p style={{ margin: 0, color: '#666', fontSize: '0.85rem' }}>{jabatan}</p>
        </div>
      </div>
    </div>
  );
}

// Penggunaan:
function HalamanTestimoni() {
  return (
    <div>
      <KartuTestimoni 
        nama="Rina Wulandari" 
        jabatan="CEO Startup XYZ"
        foto="rina.jpg"
        rating={5}
      >
        Produk ini benar-benar mengubah cara kerja tim kami. 
        Sangat recommended untuk semua startup!
      </KartuTestimoni>
      
      <KartuTestimoni 
        nama="Budi Hartono" 
        jabatan="Freelance Designer"
        foto="budi.jpg"
        rating={4}
      >
        Fiturnya lengkap dan mudah digunakan. 
        Cuma kadang agak lambat kalau data banyak.
      </KartuTestimoni>
    </div>
  );
}

Challenge 3: Komponen Wrapper Reusable

Buat komponen Panel yang:

  • Menerima judul, icon (emoji), collapsible (boolean), dan children
  • Kalau collapsible={true}, tampilkan tombol "Buka/Tutup" (pakai state sederhana dengan useState)
  • Kalau collapsible={false}, konten selalu terlihat
  • Default: nggak collapsible

Bonus: Buat juga komponen PageLayout yang menerima header, sidebar, dan children sebagai props terpisah (bukan cuma children).

💡 Hint
  • Import useState dari React: import { useState } from 'react'
  • const [buka, setBuka] = useState(true) untuk state buka/tutup
  • Conditional rendering: {buka && <div>{children}</div>}
  • Untuk PageLayout, terima header dan sidebar sebagai props biasa (bisa berisi JSX)
✅ Solusi
jsx
import { useState } from 'react';

function Panel({ judul, icon = "📋", collapsible = false, children }) {
  const [buka, setBuka] = useState(true);
  
  return (
    <div style={{
      border: '1px solid #e0e0e0',
      borderRadius: '8px',
      margin: '12px 0',
      overflow: 'hidden'
    }}>
      {/* Header */}
      <div style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        padding: '12px 16px',
        backgroundColor: '#f5f5f5',
        borderBottom: buka ? '1px solid #e0e0e0' : 'none'
      }}>
        <span style={{ fontWeight: 'bold' }}>
          {icon} {judul}
        </span>
        {collapsible && (
          <button 
            onClick={() => setBuka(!buka)}
            style={{
              background: 'none',
              border: '1px solid #ccc',
              borderRadius: '4px',
              padding: '4px 8px',
              cursor: 'pointer'
            }}
          >
            {buka ? '🔼 Tutup' : '🔽 Buka'}
          </button>
        )}
      </div>
      
      {/* Konten */}
      {buka && (
        <div style={{ padding: '16px' }}>
          {children}
        </div>
      )}
    </div>
  );
}

// PageLayout dengan multiple "slot" props
function PageLayout({ header, sidebar, children }) {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: '250px 1fr', minHeight: '100vh' }}>
      {/* Header spanning full width */}
      <header style={{ 
        gridColumn: '1 / -1', 
        padding: '16px', 
        backgroundColor: '#1976d2',
        color: 'white'
      }}>
        {header}
      </header>
      
      {/* Sidebar */}
      <aside style={{ padding: '16px', backgroundColor: '#f5f5f5' }}>
        {sidebar}
      </aside>
      
      {/* Main content */}
      <main style={{ padding: '24px' }}>
        {children}
      </main>
    </div>
  );
}

// Penggunaan:
function App() {
  return (
    <>
      <Panel judul="Informasi Pengiriman" icon="🚚" collapsible={true}>
        <p>Pesanan kamu sedang dalam perjalanan!</p>
        <p>Estimasi tiba: 2-3 hari kerja</p>
      </Panel>
      
      <Panel judul="Detail Produk" icon="📦">
        <p>Sepatu Sneakers Premium</p>
        <p>Ukuran: 42</p>
        <p>Warna: Hitam</p>
      </Panel>
      
      <PageLayout
        header={<h1>Toko Online Kita</h1>}
        sidebar={
          <nav>
            <ul>
              <li>Beranda</li>
              <li>Produk</li>
              <li>Keranjang</li>
            </ul>
          </nav>
        }
      >
        <h2>Selamat Datang!</h2>
        <p>Ini adalah konten utama halaman.</p>
      </PageLayout>
    </>
  );
}

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.