Kenapa ini penting
Hampir semua aplikasi menampilkan daftar: produk di toko online, chat di aplikasi pesan, tugas di to-do list, hasil pencarian. Datanya biasanya berupa array, dan kamu tidak mungkin menulis JSX satu per satu untuk ratusan item. React memakai cara JavaScript murni: map untuk mengubah array data menjadi array elemen, filter untuk menyaring. Bagian paling penting di modul ini adalah key: atribut kecil yang kalau salah dipakai bisa membuat input tertukar, animasi kacau, dan state nyasar ke item lain.
Konsep inti
map: dari data ke elemen
array.map() mengembalikan array baru. Kalau setiap item diubah menjadi JSX, hasilnya adalah array elemen yang bisa langsung ditaruh di dalam {}:
<ul>{buah.map(b => <li key={b.id}>{b.nama}</li>)}</ul>
Perhatikan arrow function: dengan kurung biasa => (...) ia langsung mengembalikan nilai; dengan kurung kurawal => { ... } kamu wajib menulis return.
key: KTP untuk setiap item
key adalah identitas unik setiap item di antara saudaranya. React memakai key untuk mencocokkan item di render lama dengan item di render baru: mana yang tetap, mana yang baru, mana yang dihapus, dan mana yang pindah posisi.
Analogi: guru mengabsen murid. Kalau guru mengenali murid dari nomor kursi (index), begitu ada murid pindah kursi, catatan nilai jadi tertukar. Kalau guru mengenali dari nomor induk siswa (id), siapa pun duduk di mana, catatannya tetap benar.
Aturan key:
- Unik di antara saudara (tidak harus unik di seluruh aplikasi).
- Stabil: tidak berubah antar render. Jangan pakai
Math.random()ataucrypto.randomUUID()saat render, karena setiap render menghasilkan key baru dan React akan membuat ulang semua elemen. - Sumber terbaik: id dari data (id database, id yang dibuat saat item ditambahkan).
keyditaruh di elemen terluar yang dikembalikanmap, bukan di elemen di dalamnya.keytidak diteruskan sebagai prop; komponen anak tidak bisa membacaprops.key. Kalau anak butuh id, kirim prop terpisah sepertiid={item.id}.
Kenapa bukan index?
Index (map((item, i) => <li key={i}>)) aman hanya jika daftar tidak pernah diurutkan ulang, difilter, atau disisipi di tengah/awal. Begitu item ditambahkan di awal, semua index bergeser: item yang tadinya index 0 sekarang index 1. React mengira item lama "berubah isi", bukan "ada item baru". Akibatnya, state milik komponen (misalnya teks di input atau checkbox) menempel ke posisi, bukan ke data. Contoh kode di bawah memperlihatkan bug ini secara nyata.
| Sumber key | Aman? | Catatan |
|---|---|---|
item.id dari data |
✅ | Pilihan terbaik |
| Index array | ⚠️ | Hanya untuk daftar statis yang tidak berubah urutan |
Math.random() saat render |
❌ | Berubah setiap render, semua elemen dibuat ulang |
Gabungan unik, mis. ${kategori}-${nama} |
✅ | Jika dijamin unik dan stabil |
filter + render
Untuk pencarian atau kategori, saring dulu lalu render: produk.filter(p => p.nama.includes(kata)).map(...). Hasil filter sebaiknya dihitung saat render dari state yang ada, bukan disimpan sebagai state terpisah. Ini menghindari dua sumber data yang bisa tidak sinkron.
Empty state
Daftar kosong tanpa penjelasan membuat pengguna bingung: "loading-kah? error-kah?". Selalu sediakan empty state: pesan ramah seperti "Belum ada tugas, yuk tambah satu!" atau "Produk dengan kata 'xyz' tidak ditemukan".
Fragment dengan key
Kalau setiap item menghasilkan beberapa elemen sejajar tanpa pembungkus, pakai <Fragment key={...}> (impor dari react), karena bentuk pendek <> tidak bisa diberi key.
Contoh kode
// src/App.jsx, katalog dengan pencarian, filter kategori, dan empty state
import { useState } from 'react';
const PRODUK = [
{ id: 'p1', nama: 'Kopi Gayo', kategori: 'kopi', harga: 85000 },
{ id: 'p2', nama: 'Kopi Toraja', kategori: 'kopi', harga: 90000 },
{ id: 'p3', nama: 'Teh Melati', kategori: 'teh', harga: 30000 },
{ id: 'p4', nama: 'Teh Hijau', kategori: 'teh', harga: 35000 },
{ id: 'p5', nama: 'Cokelat Bubuk', kategori: 'cokelat', harga: 45000 },
];
function KartuProduk({ nama, harga }) {
return (
<li>
<strong>{nama}</strong> - Rp {harga.toLocaleString('id-ID')}
</li>
);
}
export default function App() {
const [kata, setKata] = useState('');
const [kategori, setKategori] = useState('semua');
// Dihitung saat render, bukan disimpan di state
const hasil = PRODUK.filter(p => kategori === 'semua' || p.kategori === kategori).filter(p =>
p.nama.toLowerCase().includes(kata.toLowerCase()),
);
const kategoriUnik = ['semua', ...new Set(PRODUK.map(p => p.kategori))];
return (
<main>
<input value={kata} onChange={e => setKata(e.target.value)} placeholder="Cari produk..." />
<div>
{kategoriUnik.map(k => (
<button key={k} onClick={() => setKategori(k)} style={{ fontWeight: k === kategori ? 'bold' : 'normal' }}>
{k}
</button>
))}
</div>
<p>{hasil.length} produk ditemukan</p>
{hasil.length === 0 ? (
<p>😕 Tidak ada produk untuk "{kata}" di kategori {kategori}.</p>
) : (
<ul>
{hasil.map(p => (
// key di elemen terluar dari map; id dikirim terpisah jika anak butuh
<KartuProduk key={p.id} nama={p.nama} harga={p.harga} />
))}
</ul>
)}
</main>
);
}
// Ketik "teh" → 2 produk: Teh Melati, Teh Hijau
// Ketik "susu" → "😕 Tidak ada produk untuk "susu" di kategori semua."
Demonstrasi bug key index vs key id:
import { useState } from 'react';
function BarisTamu({ nama }) {
// State lokal di setiap baris: catatan yang diketik pengguna
const [catatan, setCatatan] = useState('');
return (
<li>
{nama}: <input value={catatan} onChange={e => setCatatan(e.target.value)} placeholder="catatan" />
</li>
);
}
export function DemoKey() {
const [tamu, setTamu] = useState([
{ id: 1, nama: 'Ayu' },
{ id: 2, nama: 'Bima' },
]);
const [nextId, setNextId] = useState(3);
function tambahDiAwal() {
setTamu([{ id: nextId, nama: `Tamu ${nextId}` }, ...tamu]);
setNextId(nextId + 1);
}
return (
<>
<button onClick={tambahDiAwal}>Tambah tamu di awal</button>
<h4>❌ key = index</h4>
<ul>{tamu.map((t, i) => <BarisTamu key={i} nama={t.nama} />)}</ul>
<h4>✅ key = id</h4>
<ul>{tamu.map(t => <BarisTamu key={t.id} nama={t.nama} />)}</ul>
</>
);
}
// Ketik "VIP" di catatan Ayu pada kedua daftar, lalu klik "Tambah tamu di awal":
// ❌ index → "VIP" sekarang menempel di "Tamu 3" (salah orang!)
// ✅ id → "VIP" tetap di Ayu
Satu item menghasilkan dua elemen:
import { Fragment } from 'react';
const ISTILAH = [
{ id: 'jsx', kata: 'JSX', arti: 'Sintaks mirip HTML di JavaScript' },
{ id: 'props', kata: 'Props', arti: 'Data dari induk ke anak' },
];
export function Glosarium() {
return (
<dl>
{ISTILAH.map(i => (
<Fragment key={i.id}>
<dt>{i.kata}</dt>
<dd>{i.arti}</dd>
</Fragment>
))}
</dl>
);
}
Kesalahan umum
- Lupa
key. Console memberi peringatan "Each child in a list should have a unique key prop". Tambahkan key dari id data. - Memakai index sebagai key untuk daftar yang bisa berubah urutan. State dan input bisa tertukar antar item. Pakai id yang stabil.
- Membuat key acak saat render.
key={Math.random()}membuat React menghancurkan dan membuat ulang semua item setiap render. Buat id sekali saat data dibuat. - Lupa
returndi arrow function berkurung kurawal.map(p => { <li>{p.nama}</li> })menghasilkan arrayundefined. Pakaimap(p => (<li>...</li>))atau tulisreturn. - Menyimpan hasil filter di state terpisah. Data jadi ganda dan mudah tidak sinkron. Hitung saja saat render.
Latihan
- Mudah, Render array
['Senin', 'Selasa', 'Rabu']menjadi daftar<li>. Karena daftarnya statis, jelaskan kenapa nama hari sendiri bisa dipakai sebagai key. - Sedang, Dari array siswa
{ id, nama, nilai }, tampilkan hanya yang nilainya ≥ 75 dengan label "Lulus", diurutkan dari nilai tertinggi (jangan mutasi array asli), dan empty state jika tidak ada. - Menantang, Buat to-do list sederhana: input + tombol tambah (id dibuat dengan
crypto.randomUUID()saat item dibuat), tombol hapus per item, filter "Semua/Selesai/Belum", dan empty state yang berbeda untuk tiap filter.
Lihat solusi
1.
const HARI = ['Senin', 'Selasa', 'Rabu'];
export default function Hari() {
return (
<ul>
{HARI.map(h => <li key={h}>{h}</li>)}
</ul>
);
}
Nama hari unik di antara saudaranya dan tidak pernah berubah, jadi memenuhi syarat key: unik dan stabil.
2.
const SISWA = [
{ id: 1, nama: 'Ani', nilai: 88 },
{ id: 2, nama: 'Bayu', nilai: 70 },
{ id: 3, nama: 'Citra', nilai: 95 },
{ id: 4, nama: 'Dodi', nilai: 75 },
];
export default function DaftarLulus() {
const lulus = SISWA.filter(s => s.nilai >= 75).toSorted((a, b) => b.nilai - a.nilai);
if (lulus.length === 0) return <p>Belum ada siswa yang lulus.</p>;
return (
<ol>
{lulus.map(s => (
<li key={s.id}>
{s.nama} - {s.nilai} (Lulus)
</li>
))}
</ol>
);
}
// → 1. Citra, 95, 2. Ani, 88, 3. Dodi, 75
toSorted mengembalikan array baru tanpa mengubah SISWA. Alternatif: [...arr].sort(...).
3.
import { useState } from 'react';
export default function Todo() {
const [todos, setTodos] = useState([]);
const [teks, setTeks] = useState('');
const [filter, setFilter] = useState('semua');
function tambah(e) {
e.preventDefault();
if (!teks.trim()) return;
setTodos([...todos, { id: crypto.randomUUID(), teks: teks.trim(), selesai: false }]);
setTeks('');
}
const tampil = todos.filter(t =>
filter === 'semua' ? true : filter === 'selesai' ? t.selesai : !t.selesai,
);
const pesanKosong = {
semua: 'Belum ada tugas. Yuk tambah satu!',
selesai: 'Belum ada tugas yang selesai.',
belum: 'Hore, semua tugas sudah selesai! 🎉',
};
return (
<>
<form onSubmit={tambah}>
<input value={teks} onChange={e => setTeks(e.target.value)} />
<button>Tambah</button>
</form>
{['semua', 'selesai', 'belum'].map(f => (
<button key={f} onClick={() => setFilter(f)} disabled={f === filter}>{f}</button>
))}
{tampil.length === 0 ? (
<p>{pesanKosong[filter]}</p>
) : (
<ul>
{tampil.map(t => (
<li key={t.id}>
<input
type="checkbox"
checked={t.selesai}
onChange={() => setTodos(todos.map(x => (x.id === t.id ? { ...x, selesai: !x.selesai } : x)))}
/>
{t.teks}
<button onClick={() => setTodos(todos.filter(x => x.id !== t.id))}>Hapus</button>
</li>
))}
</ul>
)}
</>
);
}
Ringkasan
- Render daftar dengan
array.map()yang mengembalikan JSX; gabungkan denganfilteruntuk menyaring. - Setiap item wajib punya
keyyang unik di antara saudara dan stabil antar render, idealnya id dari data. - Index sebagai key berbahaya untuk daftar yang bisa diurutkan, difilter, atau disisipi, karena state bisa tertukar.
- Hitung data turunan (hasil filter/urut) saat render, jangan disimpan sebagai state terpisah.
- Selalu sediakan empty state yang jelas, dan pakai
<Fragment key>jika satu item menghasilkan beberapa elemen.