Bab 5: Passing Props ke Komponen
⏱ 5 menit bacaProps: 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.
// 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):
// 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:
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 kurawalnama={"Budi"} - Selain string (number, boolean, array, object, function): harus pakai kurung kurawal
// 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)
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!)
// 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:
- Langsung keliatan props apa aja yang dipakai komponen ini
- Nggak perlu nulis
props.berulang-ulang - Kode lebih bersih dan mudah dibaca
// 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."
// 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// 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 ??:
||menganggap0,"",falsesebagai "kosong" (fallback ke default)??cuma fallback kalau nilainyanullatauundefined
// 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 ...:
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)
// 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}
}// 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.
// Ini:
<Tombol>Klik Saya</Tombol>
// Sama dengan ini:
<Tombol children="Klik Saya" />children bikin komponen jadi kayak "wadah" yang bisa diisi apapun:
// 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>
</>
);
}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
// 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
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.
// ❌ 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>;
}// ❌ 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?
-
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.
-
One-way data flow: Data di React mengalir satu arah: dari atas ke bawah (parent ke child). Ini bikin aplikasi lebih mudah di-debug.
-
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:
| Props | State | |
|---|---|---|
| Siapa yang "punya"? | Parent | Komponen itu sendiri |
| Bisa diubah? | ❌ Tidak (read-only) | ✅ Ya (pakai setState) |
| Dari mana? | Dikirim dari luar | Dibuat di dalam komponen |
| Analoginya | Pesanan dari pelanggan | Catatan internal dapur |
// 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
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
// 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)
// 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
// 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
// ❌ 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
// ❌ 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
// ❌ 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
// 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)
// ❌ 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
| Konsep | Penjelasan |
|---|---|
| Props | Data yang dikirim parent ke child |
| Destructuring | { nama, umur } langsung di parameter |
| Default values | { nama = "Anonim" } untuk nilai bawaan |
| Spread | {...objek} untuk forward semua props |
| Children | Konten di antara tag pembuka dan penutup |
| Read-only | Props TIDAK BOLEH diubah oleh child |
| Callback props | Fungsi 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 warnaukuran("sm", "md", "lg") yang mengubah padding dan font-sizedisabled(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
disableddi style (opacity) dan di atribut button
✅ Solusi
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
childrenuntuk isi testimoni (bisa teks apapun) - Menampilkan bintang sesuai rating
- Punya border warna emas kalau rating 5
💡 Hint
childrenotomatis tersedia sebagai prop'⭐'.repeat(rating)untuk bintang- Conditional style:
border: rating === 5 ? '2px solid gold' : '1px solid #ddd'
✅ Solusi
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), danchildren - Kalau
collapsible={true}, tampilkan tombol "Buka/Tutup" (pakai state sederhana denganuseState) - 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
useStatedari React:import { useState } from 'react' const [buka, setBuka] = useState(true)untuk state buka/tutup- Conditional rendering:
{buka && <div>{children}</div>} - Untuk PageLayout, terima
headerdansidebarsebagai props biasa (bisa berisi JSX)
✅ Solusi
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.