Bab 7: Rendering List (Menampilkan Daftar)
⏱ 4 menit bacaDari Satu Jadi Banyak
Coba buka Instagram, Twitter, atau marketplace manapun. Apa yang kamu lihat? Daftar. Daftar postingan, daftar produk, daftar komentar, daftar notifikasi. Hampir semua aplikasi modern itu intinya menampilkan daftar data.
Di React, kamu nggak perlu nulis elemen satu-satu secara manual. Bayangin kamu punya 100 produk. Masa iya nulis <KartuProduk /> 100 kali? Nggak dong. Kamu pakai array method JavaScript untuk mengubah data jadi elemen-elemen JSX secara otomatis.
// ❌ Cara manual - nggak scalable
function DaftarBuah() {
return (
<ul>
<li>Apel</li>
<li>Mangga</li>
<li>Jeruk</li>
<li>Durian</li>
<li>Rambutan</li>
</ul>
);
}
// ✅ Cara dinamis - pakai .map()
function DaftarBuah() {
const buah = ["Apel", "Mangga", "Jeruk", "Durian", "Rambutan"];
return (
<ul>
{buah.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}.map() : Senjata Utama
Method .map() mengubah setiap item di array menjadi sesuatu yang lain. Analoginya: bayangin kamu punya sekeranjang buah mentah. .map() itu kayak mesin yang mengubah setiap buah jadi jus. Buah masuk, jus keluar. Jumlahnya sama, tapi bentuknya beda.
// JavaScript biasa:
const angka = [1, 2, 3, 4, 5];
const dikaliDua = angka.map(n => n * 2);
// Hasil: [2, 4, 6, 8, 10]
// Di React - ubah data jadi JSX:
const nama = ["Budi", "Siti", "Ahmad"];
const elemenJSX = nama.map(n => <p key={n}>Halo, {n}!</p>);
// Hasil: [<p>Halo, Budi!</p>, <p>Halo, Siti!</p>, <p>Halo, Ahmad!</p>]React bisa me-render array of JSX elements. Jadi kalau kamu taruh array berisi elemen-elemen JSX di dalam {}, React akan menampilkan semuanya.
Contoh Dasar: Daftar Sederhana
function DaftarMenu() {
const menu = [
{ id: 1, nama: "Nasi Goreng", harga: 15000 },
{ id: 2, nama: "Mie Ayam", harga: 12000 },
{ id: 3, nama: "Soto Ayam", harga: 18000 },
{ id: 4, nama: "Gado-gado", harga: 14000 },
{ id: 5, nama: "Es Teh Manis", harga: 5000 },
];
return (
<div>
<h2>🍽️ Menu Warung Pak Joko</h2>
<ul>
{menu.map((item) => (
<li key={item.id}>
{item.nama} - Rp {item.harga.toLocaleString('id-ID')}
</li>
))}
</ul>
</div>
);
}Coba sendiri: Edit kode di bawah dan lihat hasilnya langsung!
Contoh dengan Komponen
// Komponen untuk satu item
function KartuProduk({ nama, harga, gambar, rating }) {
return (
<div className="kartu-produk">
<img src={gambar} alt={nama} />
<h3>{nama}</h3>
<p>Rp {harga.toLocaleString('id-ID')}</p>
<p>{'⭐'.repeat(Math.round(rating))} ({rating})</p>
</div>
);
}
// Komponen daftar yang me-render banyak kartu
function DaftarProduk() {
const produk = [
{ id: 'p1', nama: "Tas Ransel", harga: 250000, gambar: "tas.jpg", rating: 4.5 },
{ id: 'p2', nama: "Sepatu Lari", harga: 450000, gambar: "sepatu.jpg", rating: 4.8 },
{ id: 'p3', nama: "Topi Baseball", harga: 85000, gambar: "topi.jpg", rating: 4.2 },
{ id: 'p4', nama: "Kaos Polos", harga: 75000, gambar: "kaos.jpg", rating: 4.0 },
];
return (
<div className="grid-produk">
{produk.map((item) => (
<KartuProduk
key={item.id}
nama={item.nama}
harga={item.harga}
gambar={item.gambar}
rating={item.rating}
/>
))}
</div>
);
}Key: Identitas Unik Setiap Item
Perhatiin di semua contoh di atas, setiap elemen yang di-render dari .map() punya prop key. Ini wajib. Kalau nggak ada, React kasih warning di console.
Kenapa Key Penting?
Analoginya gini. Bayangin kamu guru di kelas yang punya 30 murid. Setiap hari kamu absen. Kalau setiap murid punya nomor absen (key), kamu gampang tahu siapa yang masuk, siapa yang pindah tempat duduk, siapa yang keluar.
Tapi kalau nggak ada nomor absen? Kamu cuma bisa bilang "anak pertama, anak kedua, anak ketiga..." Nah, kalau ada anak baru masuk di tengah-tengah, semua nomor bergeser. Kamu bingung: "Ini anak yang sama atau beda?"
React punya masalah yang sama. Ketika list berubah (item ditambah, dihapus, atau diurutkan ulang), React perlu tahu:
- Item mana yang baru (perlu dibuat)
- Item mana yang hilang (perlu dihapus)
- Item mana yang pindah posisi (perlu dipindah, bukan dibuat ulang)
Key membantu React mengidentifikasi setiap item secara unik, sehingga update bisa dilakukan dengan efisien.
Apa yang Terjadi Tanpa Key yang Benar?
// Tanpa key yang benar, React bisa:
// 1. Re-render semua item (lambat)
// 2. Salah update item (bug visual)
// 3. Kehilangan state internal item (input value hilang, animasi reset)Contoh bug nyata:
// ❌ Pakai index sebagai key - bisa bug!
function DaftarTodo({ todos }) {
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>
<input type="checkbox" />
{todo.teks}
</li>
))}
</ul>
);
}
// Kalau item di tengah dihapus, checkbox bisa "pindah" ke item lain!
// Karena React pikir item di index 2 masih item yang sama (padahal beda)Apa yang Bagus Dijadikan Key?
✅ ID dari database/API:
{users.map(user => <UserCard key={user.id} {...user} />)}
{posts.map(post => <PostCard key={post._id} {...post} />)}✅ Nilai unik yang stabil:
{countries.map(country => <Option key={country.code} value={country.code}>{country.name}</Option>)}
{emails.map(email => <EmailRow key={email.messageId} {...email} />)}⚠️ Index sebagai key (hanya kalau nggak ada pilihan lain DAN list nggak berubah):
// Oke kalau list STATIS dan NGGAK PERNAH berubah urutan/isinya
const menuStatis = ["Beranda", "Tentang", "Kontak"];
{menuStatis.map((item, index) => <li key={index}>{item}</li>)}❌ Random value (JANGAN!):
// ❌ SALAH - key berubah setiap render, React bikin ulang semua item!
{items.map(item => <Card key={Math.random()} {...item} />)}
{items.map(item => <Card key={crypto.randomUUID()} {...item} />)}Aturan Key
- Key harus unik di antara siblings (saudara satu level). Boleh sama di list yang berbeda.
- Key harus stabil - nggak boleh berubah antar render.
- Key nggak dikirim sebagai prop - kalau butuh ID di child, kirim sebagai prop terpisah.
// Key NGGAK bisa diakses di child component!
function ListItem({ key, nama }) { // ❌ key nggak masuk sebagai prop
return <li>{key} - {nama}</li>; // key akan undefined
}
// Kalau butuh ID di child, kirim terpisah:
{items.map(item => (
<ListItem key={item.id} id={item.id} nama={item.nama} />
))}
function ListItem({ id, nama }) { // ✅ Pakai prop 'id' terpisah
return <li>{id} - {nama}</li>;
}Filter Sebelum Render
Sering banget kamu perlu menampilkan sebagian data, bukan semuanya. Pakai .filter() sebelum .map():
function DaftarProdukTersedia() {
const semuaProduk = [
{ id: 1, nama: "Nasi Goreng", harga: 15000, stok: 10 },
{ id: 2, nama: "Mie Ayam", harga: 12000, stok: 0 },
{ id: 3, nama: "Soto Ayam", harga: 18000, stok: 5 },
{ id: 4, nama: "Bakso", harga: 15000, stok: 0 },
{ id: 5, nama: "Es Jeruk", harga: 8000, stok: 20 },
];
// Filter dulu, baru map
const produkTersedia = semuaProduk.filter(p => p.stok > 0);
return (
<div>
<h2>Menu Tersedia ({produkTersedia.length} item)</h2>
<ul>
{produkTersedia.map(produk => (
<li key={produk.id}>
{produk.nama} - Rp {produk.harga.toLocaleString('id-ID')}
(stok: {produk.stok})
</li>
))}
</ul>
</div>
);
}Chaining: Filter + Sort + Map
function DaftarMahasiswaBerprestasi() {
const mahasiswa = [
{ id: 1, nama: "Rina", ipk: 3.9, jurusan: "Informatika" },
{ id: 2, nama: "Budi", ipk: 3.2, jurusan: "Informatika" },
{ id: 3, nama: "Siti", ipk: 3.7, jurusan: "Sistem Informasi" },
{ id: 4, nama: "Ahmad", ipk: 3.85, jurusan: "Informatika" },
{ id: 5, nama: "Dewi", ipk: 3.5, jurusan: "Sistem Informasi" },
{ id: 6, nama: "Joko", ipk: 3.95, jurusan: "Informatika" },
];
// Chain: filter IPK > 3.5, sort descending, lalu map ke JSX
const berprestasi = mahasiswa
.filter(m => m.ipk >= 3.7) // Hanya IPK >= 3.7
.sort((a, b) => b.ipk - a.ipk) // Urutkan dari tertinggi
.map((m, index) => ( // Ubah jadi JSX
<tr key={m.id}>
<td>{index + 1}</td>
<td>{m.nama}</td>
<td>{m.jurusan}</td>
<td>{m.ipk.toFixed(2)}</td>
</tr>
));
return (
<div>
<h2>🏆 Mahasiswa Berprestasi (IPK ≥ 3.7)</h2>
<table>
<thead>
<tr>
<th>No</th>
<th>Nama</th>
<th>Jurusan</th>
<th>IPK</th>
</tr>
</thead>
<tbody>
{berprestasi}
</tbody>
</table>
</div>
);
}Nested Lists (Daftar Bersarang)
Kadang data kamu punya struktur bersarang. Misalnya: kategori yang punya sub-item.
function MenuRestoran() {
const kategori = [
{
id: 'makanan',
nama: '🍽️ Makanan',
items: [
{ id: 'm1', nama: 'Nasi Goreng', harga: 15000 },
{ id: 'm2', nama: 'Mie Goreng', harga: 13000 },
{ id: 'm3', nama: 'Ayam Bakar', harga: 25000 },
]
},
{
id: 'minuman',
nama: '🥤 Minuman',
items: [
{ id: 'mn1', nama: 'Es Teh', harga: 5000 },
{ id: 'mn2', nama: 'Jus Alpukat', harga: 12000 },
{ id: 'mn3', nama: 'Kopi Susu', harga: 15000 },
]
},
{
id: 'snack',
nama: '🍿 Snack',
items: [
{ id: 's1', nama: 'Kentang Goreng', harga: 10000 },
{ id: 's2', nama: 'Pisang Goreng', harga: 8000 },
]
}
];
return (
<div>
<h1>Menu Restoran</h1>
{kategori.map(kat => (
<div key={kat.id} className="kategori">
<h2>{kat.nama}</h2>
<ul>
{/* Nested map - list di dalam list */}
{kat.items.map(item => (
<li key={item.id}>
{item.nama} - Rp {item.harga.toLocaleString('id-ID')}
</li>
))}
</ul>
</div>
))}
</div>
);
}Memisahkan Nested List ke Komponen Terpisah
Kalau nested list mulai kompleks, lebih baik pisahkan jadi komponen sendiri:
// Komponen untuk satu item menu
function MenuItem({ nama, harga, deskripsi }) {
return (
<div className="menu-item">
<div className="menu-info">
<h4>{nama}</h4>
{deskripsi && <p className="deskripsi">{deskripsi}</p>}
</div>
<span className="harga">Rp {harga.toLocaleString('id-ID')}</span>
</div>
);
}
// Komponen untuk satu kategori
function KategoriMenu({ nama, icon, items }) {
return (
<section className="kategori-menu">
<h3>{icon} {nama}</h3>
<div className="items-list">
{items.map(item => (
<MenuItem
key={item.id}
nama={item.nama}
harga={item.harga}
deskripsi={item.deskripsi}
/>
))}
</div>
</section>
);
}
// Komponen utama - bersih dan mudah dibaca
function MenuLengkap({ dataMenu }) {
return (
<div className="menu-lengkap">
{dataMenu.map(kategori => (
<KategoriMenu
key={kategori.id}
nama={kategori.nama}
icon={kategori.icon}
items={kategori.items}
/>
))}
</div>
);
}Transformasi Data Sebelum Render
Sering banget data dari API bentuknya nggak sesuai dengan apa yang mau ditampilkan. Kamu perlu transformasi dulu:
function LeaderboardPenjualan() {
// Data mentah dari API
const dataPenjualan = [
{ id: 1, nama: "Toko Makmur", jan: 50, feb: 45, mar: 60 },
{ id: 2, nama: "Warung Sejahtera", jan: 30, feb: 55, mar: 40 },
{ id: 3, nama: "Kios Berkah", jan: 70, feb: 65, mar: 80 },
{ id: 4, nama: "Lapak Jaya", jan: 20, feb: 25, mar: 30 },
];
// Transformasi: hitung total dan ranking
const leaderboard = dataPenjualan
.map(toko => ({
...toko,
total: toko.jan + toko.feb + toko.mar,
rataRata: ((toko.jan + toko.feb + toko.mar) / 3).toFixed(1),
}))
.sort((a, b) => b.total - a.total)
.map((toko, index) => ({
...toko,
ranking: index + 1,
medali: index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '',
}));
return (
<div>
<h2>🏆 Leaderboard Penjualan Q1</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>Toko</th>
<th>Jan</th>
<th>Feb</th>
<th>Mar</th>
<th>Total</th>
<th>Rata-rata</th>
</tr>
</thead>
<tbody>
{leaderboard.map(toko => (
<tr key={toko.id} style={{
backgroundColor: toko.ranking <= 3 ? '#fff9c4' : 'transparent'
}}>
<td>{toko.medali} {toko.ranking}</td>
<td><strong>{toko.nama}</strong></td>
<td>{toko.jan}</td>
<td>{toko.feb}</td>
<td>{toko.mar}</td>
<td><strong>{toko.total}</strong></td>
<td>{toko.rataRata}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}Grouping: Mengelompokkan Data
function KontakTerkelompok() {
const kontak = [
{ id: 1, nama: "Ahmad Fauzi", kota: "Jakarta" },
{ id: 2, nama: "Budi Santoso", kota: "Bandung" },
{ id: 3, nama: "Citra Dewi", kota: "Jakarta" },
{ id: 4, nama: "Dian Permata", kota: "Surabaya" },
{ id: 5, nama: "Eko Prasetyo", kota: "Bandung" },
{ id: 6, nama: "Fitri Handayani", kota: "Jakarta" },
];
// Kelompokkan berdasarkan kota
const perKota = kontak.reduce((grup, orang) => {
const kota = orang.kota;
if (!grup[kota]) {
grup[kota] = [];
}
grup[kota].push(orang);
return grup;
}, {});
// perKota = { Jakarta: [...], Bandung: [...], Surabaya: [...] }
return (
<div>
<h2>📇 Kontak per Kota</h2>
{Object.entries(perKota).map(([kota, orangOrang]) => (
<div key={kota} className="grup-kota">
<h3>📍 {kota} ({orangOrang.length} orang)</h3>
<ul>
{orangOrang.map(orang => (
<li key={orang.id}>{orang.nama}</li>
))}
</ul>
</div>
))}
</div>
);
}Rendering List Kosong
Selalu handle kasus ketika list kosong:
function DaftarPesanan({ pesanan }) {
// Handle list kosong
if (pesanan.length === 0) {
return (
<div className="empty-state">
<p style={{ fontSize: '3rem' }}>📭</p>
<h3>Belum Ada Pesanan</h3>
<p>Yuk mulai belanja! Banyak promo menarik lho.</p>
<button>Mulai Belanja</button>
</div>
);
}
return (
<div>
<h2>Pesanan Kamu ({pesanan.length})</h2>
{pesanan.map(p => (
<div key={p.id} className="kartu-pesanan">
<p><strong>#{p.id}</strong> - {p.produk}</p>
<p>Status: {p.status}</p>
</div>
))}
</div>
);
}Contoh Lengkap: Tabel Interaktif
import { useState } from 'react';
function TabelKaryawan() {
const [filter, setFilter] = useState('semua');
const [cari, setCari] = useState('');
const karyawan = [
{ id: 1, nama: "Andi Wijaya", departemen: "Engineering", gaji: 15000000, status: "aktif" },
{ id: 2, nama: "Budi Hartono", departemen: "Marketing", gaji: 12000000, status: "aktif" },
{ id: 3, nama: "Citra Lestari", departemen: "Engineering", gaji: 18000000, status: "aktif" },
{ id: 4, nama: "Doni Prasetyo", departemen: "HR", gaji: 11000000, status: "cuti" },
{ id: 5, nama: "Eka Putri", departemen: "Marketing", gaji: 13000000, status: "aktif" },
{ id: 6, nama: "Fajar Nugroho", departemen: "Engineering", gaji: 16000000, status: "resign" },
{ id: 7, nama: "Gita Savitri", departemen: "HR", gaji: 14000000, status: "aktif" },
];
// Filter berdasarkan status
const hasilFilter = karyawan.filter(k => {
if (filter !== 'semua' && k.status !== filter) return false;
if (cari && !k.nama.toLowerCase().includes(cari.toLowerCase())) return false;
return true;
});
return (
<div>
<h2>👥 Data Karyawan</h2>
{/* Kontrol filter */}
<div style={{ marginBottom: '16px', display: 'flex', gap: '12px' }}>
<input
type="text"
placeholder="🔍 Cari nama..."
value={cari}
onChange={(e) => setCari(e.target.value)}
style={{ padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
/>
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
style={{ padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
>
<option value="semua">Semua Status</option>
<option value="aktif">Aktif</option>
<option value="cuti">Cuti</option>
<option value="resign">Resign</option>
</select>
</div>
{/* Hasil */}
{hasilFilter.length === 0 ? (
<p style={{ color: '#999', fontStyle: 'italic' }}>
Tidak ada karyawan yang cocok dengan filter.
</p>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ backgroundColor: '#f5f5f5' }}>
<th style={{ padding: '8px', textAlign: 'left' }}>Nama</th>
<th style={{ padding: '8px', textAlign: 'left' }}>Departemen</th>
<th style={{ padding: '8px', textAlign: 'right' }}>Gaji</th>
<th style={{ padding: '8px', textAlign: 'center' }}>Status</th>
</tr>
</thead>
<tbody>
{hasilFilter.map(k => (
<tr key={k.id} style={{ borderBottom: '1px solid #eee' }}>
<td style={{ padding: '8px' }}>{k.nama}</td>
<td style={{ padding: '8px' }}>{k.departemen}</td>
<td style={{ padding: '8px', textAlign: 'right' }}>
Rp {k.gaji.toLocaleString('id-ID')}
</td>
<td style={{ padding: '8px', textAlign: 'center' }}>
<span style={{
padding: '2px 8px',
borderRadius: '12px',
fontSize: '0.8rem',
backgroundColor:
k.status === 'aktif' ? '#e8f5e9' :
k.status === 'cuti' ? '#fff3e0' : '#ffebee',
color:
k.status === 'aktif' ? '#2e7d32' :
k.status === 'cuti' ? '#e65100' : '#c62828',
}}>
{k.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
)}
<p style={{ color: '#666', fontSize: '0.9rem', marginTop: '8px' }}>
Menampilkan {hasilFilter.length} dari {karyawan.length} karyawan
</p>
</div>
);
}⚠️ Jebakan
Jebakan 1: Lupa Key
// ❌ Warning: Each child in a list should have a unique "key" prop
{items.map(item => <li>{item.nama}</li>)}
// ✅ Selalu kasih key
{items.map(item => <li key={item.id}>{item.nama}</li>)}Jebakan 2: Pakai Index Sebagai Key di List Dinamis
// ❌ Bug potensial - kalau list bisa berubah (tambah/hapus/reorder)
{todos.map((todo, index) => (
<TodoItem key={index} teks={todo.teks} />
))}
// ✅ Pakai ID yang stabil
{todos.map(todo => (
<TodoItem key={todo.id} teks={todo.teks} />
))}Kapan index BOLEH dipakai?
- List benar-benar statis (nggak pernah berubah)
- Item nggak punya ID unik
- List nggak pernah di-reorder atau di-filter
Jebakan 3: Key Duplikat
// ❌ Key harus unik! Kalau ada duplikat, React bingung
const items = [
{ id: 1, nama: "Apel" },
{ id: 1, nama: "Jeruk" }, // ID sama! Bug!
];
{items.map(item => <li key={item.id}>{item.nama}</li>)}
// React cuma render salah satu, atau behavior aneh lainnya
// ✅ Pastikan key unik - kalau data dari API punya duplikat, handle duluJebakan 4: Key di Tempat yang Salah
// ❌ Key harus di elemen TERLUAR yang di-return dari map
{items.map(item => (
<div> {/* Key harusnya di sini! */}
<h3 key={item.id}>{item.nama}</h3> {/* Salah tempat */}
<p>{item.deskripsi}</p>
</div>
))}
// ✅ Key di elemen terluar
{items.map(item => (
<div key={item.id}>
<h3>{item.nama}</h3>
<p>{item.deskripsi}</p>
</div>
))}Jebakan 5: Lupa Return di map (Arrow Function dengan )
// ❌ Pakai {} tanpa return - map menghasilkan [undefined, undefined, ...]
{items.map(item => {
<li>{item.nama}</li> // Nggak ada return!
})}
// ✅ Cara 1: Pakai () untuk implicit return
{items.map(item => (
<li key={item.id}>{item.nama}</li>
))}
// ✅ Cara 2: Pakai {} dengan explicit return
{items.map(item => {
return <li key={item.id}>{item.nama}</li>;
})}
// ✅ Cara 3: Satu baris tanpa kurung
{items.map(item => <li key={item.id}>{item.nama}</li>)}Jebakan 6: Mutasi Array Asli
// ❌ .sort() MENGUBAH array asli!
function DaftarHarga({ produk }) {
produk.sort((a, b) => a.harga - b.harga); // Mutasi props!
return (
<ul>
{produk.map(p => <li key={p.id}>{p.nama}: {p.harga}</li>)}
</ul>
);
}
// ✅ Copy dulu, baru sort
function DaftarHarga({ produk }) {
const sorted = [...produk].sort((a, b) => a.harga - b.harga);
return (
<ul>
{sorted.map(p => <li key={p.id}>{p.nama}: {p.harga}</li>)}
</ul>
);
}Ringkasan
| Konsep | Penjelasan |
|---|---|
.map() | Ubah setiap item array jadi elemen JSX |
key | Identitas unik untuk setiap item di list |
.filter() | Saring item sebelum di-render |
.sort() | Urutkan (copy dulu dengan [...arr]!) |
| Nested list | .map() di dalam .map() |
| Empty state | Handle kasus array kosong |
| Key rules | Unik, stabil, di elemen terluar |
🏋️ Challenge
Challenge 1: Daftar Belanja dengan Kategori
Buat komponen DaftarBelanja yang:
- Punya array item belanja dengan properti: id, nama, kategori ("buah", "sayur", "protein"), harga, sudahDibeli (boolean)
- Kelompokkan item berdasarkan kategori
- Tampilkan total harga per kategori
- Item yang sudah dibeli ditampilkan dengan style dicoret
- Tampilkan total keseluruhan di bawah
💡 Hint
- Pakai
.reduce()untuk grouping berdasarkan kategori Object.entries()untuk iterasi hasil groupingtextDecoration: 'line-through'untuk item yang sudah dibeli.filter(i => !i.sudahDibeli).reduce(...)untuk total yang belum dibeli
✅ Solusi
function DaftarBelanja() {
const items = [
{ id: 1, nama: "Apel", kategori: "buah", harga: 25000, sudahDibeli: true },
{ id: 2, nama: "Bayam", kategori: "sayur", harga: 8000, sudahDibeli: false },
{ id: 3, nama: "Ayam", kategori: "protein", harga: 35000, sudahDibeli: false },
{ id: 4, nama: "Jeruk", kategori: "buah", harga: 20000, sudahDibeli: false },
{ id: 5, nama: "Wortel", kategori: "sayur", harga: 12000, sudahDibeli: true },
{ id: 6, nama: "Telur", kategori: "protein", harga: 28000, sudahDibeli: false },
{ id: 7, nama: "Mangga", kategori: "buah", harga: 30000, sudahDibeli: false },
{ id: 8, nama: "Tahu", kategori: "protein", harga: 10000, sudahDibeli: true },
];
// Grouping berdasarkan kategori
const perKategori = items.reduce((grup, item) => {
if (!grup[item.kategori]) {
grup[item.kategori] = [];
}
grup[item.kategori].push(item);
return grup;
}, {});
// Emoji per kategori
const emojiKategori = { buah: '🍎', sayur: '🥬', protein: '🍗' };
// Total keseluruhan
const totalSemua = items.reduce((sum, item) => sum + item.harga, 0);
const totalBelumDibeli = items
.filter(i => !i.sudahDibeli)
.reduce((sum, item) => sum + item.harga, 0);
return (
<div style={{ maxWidth: '500px', padding: '20px' }}>
<h2>🛒 Daftar Belanja</h2>
{Object.entries(perKategori).map(([kategori, itemList]) => {
const totalKategori = itemList.reduce((sum, i) => sum + i.harga, 0);
return (
<div key={kategori} style={{ marginBottom: '20px' }}>
<h3>
{emojiKategori[kategori]} {kategori.charAt(0).toUpperCase() + kategori.slice(1)}
<span style={{ fontSize: '0.8rem', color: '#666', marginLeft: '8px' }}>
(Rp {totalKategori.toLocaleString('id-ID')})
</span>
</h3>
<ul style={{ listStyle: 'none', padding: 0 }}>
{itemList.map(item => (
<li key={item.id} style={{
padding: '8px',
display: 'flex',
justifyContent: 'space-between',
textDecoration: item.sudahDibeli ? 'line-through' : 'none',
color: item.sudahDibeli ? '#999' : '#333',
borderBottom: '1px solid #eee'
}}>
<span>{item.sudahDibeli ? '✅' : '⬜'} {item.nama}</span>
<span>Rp {item.harga.toLocaleString('id-ID')}</span>
</li>
))}
</ul>
</div>
);
})}
{/* Total */}
<div style={{
borderTop: '2px solid #333',
paddingTop: '12px',
marginTop: '12px'
}}>
<p><strong>Total semua: Rp {totalSemua.toLocaleString('id-ID')}</strong></p>
<p style={{ color: '#666' }}>
Sisa belum dibeli: Rp {totalBelumDibeli.toLocaleString('id-ID')}
</p>
</div>
</div>
);
}Challenge 2: Galeri Foto dengan Filter
Buat komponen GaleriFoto yang:
- Punya array foto dengan: id, judul, kategori ("alam", "kota", "makanan"), url, likes
- Ada tombol filter per kategori + "Semua"
- Ada opsi sort: "Terbaru" (by id desc) atau "Terpopuler" (by likes desc)
- Tampilkan jumlah foto yang ditampilkan
- Pakai
useStateuntuk filter dan sort aktif
💡 Hint
useStateuntukfilterAktifdansortBy- Chain:
.filter().sort().map() - Tombol filter aktif bisa dikasih style berbeda (bold/warna)
- Jangan lupa
[...arr].sort()supaya nggak mutasi
✅ Solusi
import { useState } from 'react';
function GaleriFoto() {
const [filterAktif, setFilterAktif] = useState('semua');
const [sortBy, setSortBy] = useState('terbaru');
const foto = [
{ id: 1, judul: "Gunung Bromo", kategori: "alam", url: "bromo.jpg", likes: 245 },
{ id: 2, judul: "Kota Tua Jakarta", kategori: "kota", url: "kota-tua.jpg", likes: 189 },
{ id: 3, judul: "Nasi Padang", kategori: "makanan", url: "nasi-padang.jpg", likes: 312 },
{ id: 4, judul: "Pantai Kuta", kategori: "alam", url: "kuta.jpg", likes: 456 },
{ id: 5, judul: "Skyline Surabaya", kategori: "kota", url: "surabaya.jpg", likes: 134 },
{ id: 6, judul: "Rendang", kategori: "makanan", url: "rendang.jpg", likes: 521 },
{ id: 7, judul: "Danau Toba", kategori: "alam", url: "toba.jpg", likes: 389 },
{ id: 8, judul: "Sate Madura", kategori: "makanan", url: "sate.jpg", likes: 278 },
];
const kategoriList = ['semua', 'alam', 'kota', 'makanan'];
const emojiKategori = { semua: '📷', alam: '🏔️', kota: '🏙️', makanan: '🍜' };
// Filter dan sort
const hasilFoto = [...foto]
.filter(f => filterAktif === 'semua' || f.kategori === filterAktif)
.sort((a, b) => {
if (sortBy === 'terbaru') return b.id - a.id;
return b.likes - a.likes;
});
return (
<div style={{ maxWidth: '800px' }}>
<h2>📸 Galeri Foto Indonesia</h2>
{/* Filter buttons */}
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
{kategoriList.map(kat => (
<button
key={kat}
onClick={() => setFilterAktif(kat)}
style={{
padding: '8px 16px',
border: 'none',
borderRadius: '20px',
cursor: 'pointer',
backgroundColor: filterAktif === kat ? '#1976d2' : '#e0e0e0',
color: filterAktif === kat ? 'white' : '#333',
fontWeight: filterAktif === kat ? 'bold' : 'normal',
}}
>
{emojiKategori[kat]} {kat.charAt(0).toUpperCase() + kat.slice(1)}
</button>
))}
</div>
{/* Sort */}
<div style={{ marginBottom: '16px' }}>
<label>Urutkan: </label>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="terbaru">Terbaru</option>
<option value="populer">Terpopuler</option>
</select>
<span style={{ marginLeft: '12px', color: '#666' }}>
({hasilFoto.length} foto)
</span>
</div>
{/* Grid foto */}
{hasilFoto.length === 0 ? (
<p>Tidak ada foto untuk kategori ini.</p>
) : (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
gap: '16px'
}}>
{hasilFoto.map(f => (
<div key={f.id} style={{
border: '1px solid #ddd',
borderRadius: '8px',
overflow: 'hidden'
}}>
<img
src={f.url}
alt={f.judul}
style={{ width: '100%', height: '150px', objectFit: 'cover' }}
/>
<div style={{ padding: '8px' }}>
<h4 style={{ margin: '0 0 4px' }}>{f.judul}</h4>
<div style={{ display: 'flex', justifyContent: 'space-between', color: '#666', fontSize: '0.85rem' }}>
<span>{emojiKategori[f.kategori]} {f.kategori}</span>
<span>❤️ {f.likes}</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
);
}Challenge 3: Generate Key yang Benar
Perhatikan kode berikut yang punya bug terkait key. Perbaiki semua masalahnya dan jelaskan kenapa:
function DaftarKontak({ kontak }) {
return (
<ul>
{kontak.map((orang, index) => (
<li key={Math.random()}>
<img key={index} src={orang.foto} alt={orang.nama} />
<span>{orang.nama}</span>
<span>{orang.email}</span>
</li>
))}
</ul>
);
}💡 Hint
Ada 3 masalah:
Math.random()sebagai key (berubah setiap render)- Key di
<img>nggak perlu (bukan hasil dari map) - Harusnya pakai ID yang stabil dari data
✅ Solusi
function DaftarKontak({ kontak }) {
return (
<ul>
{kontak.map((orang) => (
// ✅ Pakai ID stabil dari data, bukan Math.random()
<li key={orang.id}>
{/* ✅ Hapus key dari img - ini bukan hasil map, nggak perlu key */}
<img src={orang.foto} alt={orang.nama} />
<span>{orang.nama}</span>
<span>{orang.email}</span>
</li>
))}
</ul>
);
}
/*
Penjelasan masalah:
1. Math.random() sebagai key:
- Key HARUS stabil antar render
- Math.random() menghasilkan nilai baru setiap render
- Akibatnya: React menganggap SEMUA item baru setiap render
- Semua item di-unmount dan di-mount ulang (lambat, state hilang)
2. Key di <img> yang bukan dari map:
- Key cuma dibutuhkan untuk elemen yang dihasilkan dari iterasi (map)
- <img> di sini adalah child statis dari <li>, nggak perlu key
- Menambahkan key yang nggak perlu nggak error, tapi membingungkan
3. Solusi: pakai orang.id (atau orang.email kalau unik)
- ID dari database/API selalu stabil dan unik
- Ini memungkinkan React melacak item dengan benar
*/Sudah paham materi ini?
Tandai sebagai selesai untuk melacak progress-mu.