Bab 7: Rendering List (Menampilkan Daftar)

4 menit baca

Dari 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.

jsx
// ❌ 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
// 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

jsx
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

jsx
// 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?

jsx
// 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:

jsx
// ❌ 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:

jsx
{users.map(user => <UserCard key={user.id} {...user} />)}
{posts.map(post => <PostCard key={post._id} {...post} />)}

✅ Nilai unik yang stabil:

jsx
{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):

jsx
// 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!):

jsx
// ❌ 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

  1. Key harus unik di antara siblings (saudara satu level). Boleh sama di list yang berbeda.
  2. Key harus stabil - nggak boleh berubah antar render.
  3. Key nggak dikirim sebagai prop - kalau butuh ID di child, kirim sebagai prop terpisah.
jsx
// 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():

jsx
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

jsx
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.

jsx
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:

jsx
// 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:

jsx
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

jsx
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:

jsx
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

jsx
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

jsx
// ❌ 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

jsx
// ❌ 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

jsx
// ❌ 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 dulu

Jebakan 4: Key di Tempat yang Salah

jsx
// ❌ 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 )

jsx
// ❌ 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

jsx
// ❌ .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

KonsepPenjelasan
.map()Ubah setiap item array jadi elemen JSX
keyIdentitas 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 stateHandle kasus array kosong
Key rulesUnik, 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 grouping
  • textDecoration: 'line-through' untuk item yang sudah dibeli
  • .filter(i => !i.sudahDibeli).reduce(...) untuk total yang belum dibeli
✅ Solusi
jsx
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 useState untuk filter dan sort aktif
💡 Hint
  • useState untuk filterAktif dan sortBy
  • Chain: .filter().sort().map()
  • Tombol filter aktif bisa dikasih style berbeda (bold/warna)
  • Jangan lupa [...arr].sort() supaya nggak mutasi
✅ Solusi
jsx
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:

jsx
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:

  1. Math.random() sebagai key (berubah setiap render)
  2. Key di <img> nggak perlu (bukan hasil dari map)
  3. Harusnya pakai ID yang stabil dari data
✅ Solusi
jsx
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.