Cara Mengatasi Error ‘Keys’ Unik saat Rendering List di React
Pendahuluan
Saat pertama kali belajar React, hampir setiap developer pasti pernah melihat pesan peringatan seperti ini di console browser:
Warning: Each child in a list should have a unique "key" prop.
Pesan ini terlihat sepele, tapi kalau diabaikan, kamu akan menghadapi bug yang sulit dilacak — mulai dari UI yang tidak ter-update dengan benar, hingga performa aplikasi yang menurun drastis. Bayangkan kamu sedang membangun aplikasi seperti Tokopedia atau Shopee yang menampilkan ribuan produk dalam list — tanpa key yang tepat, performa render bisa menjadi mimpi buruk.
Artikel ini akan menjelaskan secara mendalam mengapa key itu penting, bagaimana cara menggunakannya dengan benar, serta pola anti-pattern yang harus kamu hindari.
Mengapa key Dibutuhkan oleh React?
React menggunakan mekanisme bernama Reconciliation — proses di mana React membandingkan Virtual DOM lama dengan yang baru untuk menentukan perubahan minimal yang perlu diterapkan ke DOM asli.
Ketika kamu merender sebuah list, React perlu tahu elemen mana yang:
- Baru ditambahkan
- Dihapus
- Dipindahkan posisinya
Tanpa key, React tidak punya cara efisien untuk membedakan satu elemen list dari elemen lainnya. Ia harus membandingkan semuanya secara urutan, yang sering mengakibatkan render yang salah atau tidak perlu.
Perhatikan ilustrasi ini:
// Sebelum update
<ul>
<li>Apel</li>
<li>Mangga</li>
<li>Durian</li>
</ul>
// Setelah item "Pisang" ditambahkan di awal
<ul>
<li>Pisang</li>
<li>Apel</li>
<li>Mangga</li>
<li>Durian</li>
</ul>
Tanpa key, React mengira ketiga <li> pertama berubah nilainya, lalu menambahkan satu <li> baru di akhir — padahal seharusnya hanya satu elemen baru yang ditambahkan di awal. Ini membuat React melakukan 4 operasi DOM yang tidak perlu, padahal cukup 1.
Dengan key yang unik dan stabil, React langsung tahu bahwa “Apel”, “Mangga”, dan “Durian” tidak berubah — hanya perlu menyisipkan “Pisang” di posisi pertama.
Cara Memberikan key yang Tepat pada Elemen List
Aturan utama: key harus unik di antara sibling (saudara kandung dalam list yang sama) dan stabil (tidak berubah antar render).
Menggunakan ID dari Data
Cara terbaik adalah menggunakan ID unik yang sudah ada di data kamu:
const produkList = [
{ id: "prod-001", nama: "Laptop Gaming", harga: 15000000 },
{ id: "prod-002", nama: "Mouse Wireless", harga: 250000 },
{ id: "prod-003", nama: "Keyboard Mechanical", harga: 750000 },
];
function DaftarProduk() {
return (
<ul>
{produkList.map((produk) => (
<li key={produk.id}>
{produk.nama} — Rp {produk.harga.toLocaleString("id-ID")}
</li>
))}
</ul>
);
}
export default DaftarProduk;
Key pada Komponen, Bukan Elemen HTML di Dalamnya
Perlu diperhatikan: key harus diberikan pada elemen paling luar yang dikembalikan dalam .map():
// ✅ Benar — key ada di komponen terluar dalam map
function DaftarPengguna({ users }) {
return (
<div>
{users.map((user) => (
<KartuPengguna key={user.id} user={user} />
))}
</div>
);
}
// ❌ Salah — key tidak ditempatkan di elemen yang di-return map
function DaftarPengguna({ users }) {
return (
<div>
{users.map((user) => (
<KartuPengguna user={user} /> {/* lupa key! */}
))}
</div>
);
}
Key Harus Unik di Antara Sibling, Bukan Secara Global
key tidak perlu unik secara global di seluruh aplikasi — cukup unik di antara elemen-elemen dalam list yang sama:
function Halaman() {
return (
<>
{/* List pertama — key cukup unik dalam list ini */}
<ul>
{kategori.map((kat) => (
<li key={kat.id}>{kat.nama}</li>
))}
</ul>
{/* List kedua — boleh pakai key yang sama seperti list pertama */}
<ul>
{subkategori.map((sub) => (
<li key={sub.id}>{sub.nama}</li>
))}
</ul>
</>
);
}
Anti-Pattern: Bahaya Menggunakan Indeks Array sebagai key
Salah satu kesalahan paling umum adalah menggunakan indeks array sebagai key:
// ❌ Anti-pattern yang harus dihindari
{items.map((item, index) => (
<li key={index}>{item.nama}</li>
))}
Kenapa ini berbahaya?
Bayangkan kamu punya list tugas dan pengguna menghapus item di tengah-tengah:
Sebelum hapus:
key=0: "Beli bahan makanan" ← dihapus
key=1: "Bayar listrik"
key=2: "Meeting jam 3"
Setelah hapus:
key=0: "Bayar listrik" ← React kira ini item yang sama dengan "Beli bahan makanan"!
key=1: "Meeting jam 3"
React tidak tahu bahwa item dengan key=0 sudah berganti isi. Akibatnya, jika elemen tersebut punya state internal (misalnya input yang sedang diisi), state itu tidak ikut berpindah — dan UI akan menampilkan data yang salah.
Kapan indeks boleh digunakan?
Hanya dalam kondisi sangat terbatas:
- List bersifat statis (tidak pernah diubah, dihapus, atau diurutkan ulang)
- List tidak memiliki komponen dengan state internal
- Tidak ada cara lain untuk mendapatkan ID unik
// ✅ Boleh pakai index jika list benar-benar statis
const langkahInstalasi = ["Download installer", "Jalankan setup", "Restart komputer"];
function LangkahInstalasi() {
return (
<ol>
{langkahInstalasi.map((langkah, index) => (
<li key={index}>{langkah}</li>
))}
</ol>
);
}
Contoh Kasus Nyata: Membangun To-Do List
Mari kita lihat implementasi yang benar menggunakan crypto.randomUUID() untuk menghasilkan ID unik saat item dibuat:
import { useState } from "react";
function TodoApp() {
const [todos, setTodos] = useState([
{ id: crypto.randomUUID(), teks: "Belajar React", selesai: false },
{ id: crypto.randomUUID(), teks: "Buat proyek portfolio", selesai: false },
]);
const [inputBaru, setInputBaru] = useState("");
function tambahTodo() {
if (!inputBaru.trim()) return;
setTodos([
...todos,
{ id: crypto.randomUUID(), teks: inputBaru, selesai: false },
]);
setInputBaru("");
}
function hapusTodo(id) {
setTodos(todos.filter((todo) => todo.id !== id));
}
function toggleSelesai(id) {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, selesai: !todo.selesai } : todo
)
);
}
return (
<div>
<h2>Daftar Tugas</h2>
<div>
<input
value={inputBaru}
onChange={(e) => setInputBaru(e.target.value)}
placeholder="Tambah tugas baru..."
/>
<button onClick={tambahTodo}>Tambah</button>
</div>
<ul>
{todos.map((todo) => (
<li
key={todo.id}
style={{ textDecoration: todo.selesai ? "line-through" : "none" }}
>
<input
type="checkbox"
checked={todo.selesai}
onChange={() => toggleSelesai(todo.id)}
/>
{todo.teks}
<button onClick={() => hapusTodo(todo.id)}>Hapus</button>
</li>
))}
</ul>
</div>
);
}
export default TodoApp;
Perhatikan bahwa setiap todo diberi ID unik saat dibuat, bukan saat dirender. Ini memastikan key tetap stabil meskipun list diurutkan ulang atau difilter.
Jika kamu bekerja dengan data yang datang dari API (seperti yang dijelaskan di artikel Cara Mengambil Data dari API di React), pastikan backend kamu selalu mengembalikan field id yang unik untuk setiap item.
Troubleshooting: Error yang Sering Muncul
Warning: Each child in a list should have a unique “key” prop
Penyebab: Kamu merender list menggunakan .map() tanpa memberikan prop key pada elemen yang dikembalikan.
Solusi:
// ❌ Sebelum — menyebabkan warning
function DaftarItem({ items }) {
return (
<ul>
{items.map((item) => (
<li>{item.nama}</li>
))}
</ul>
);
}
// ✅ Sesudah — warning hilang
function DaftarItem({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.nama}</li>
))}
</ul>
);
}
Warning: Encountered two children with the same key
Penyebab: Ada dua atau lebih elemen dalam list yang memiliki nilai key yang sama — biasanya karena data dari API memiliki ID duplikat, atau kamu menggabungkan dua list yang ID-nya bisa bertabrakan.
Solusi:
// ✅ Jika menggabungkan dua sumber data, beri prefix untuk membedakan
function GabunganProduk({ listA, listB }) {
return (
<ul>
{[...listA, ...listB].map((item) => (
<li key={`${item.sumber}-${item.id}`}>{item.nama}</li>
))}
</ul>
);
}
// ✅ Atau filter duplikat sebelum render
function DaftarUnik({ items }) {
const uniqueItems = Array.from(
new Map(items.map((i) => [i.id, i])).values()
);
return (
<ul>
{uniqueItems.map((item) => (
<li key={item.id}>{item.nama}</li>
))}
</ul>
);
}
State input tidak berpindah saat item dihapus di tengah list
Penyebab: Kamu menggunakan indeks array sebagai key. Saat item dihapus, React salah mengidentifikasi elemen yang tersisa dan state-nya tidak ikut berpindah — akibatnya nilai input bisa “loncat” ke item yang salah.
Solusi:
// ❌ Bermasalah — pakai indeks sebagai key
function DaftarTugas({ todos }) {
return (
<ul>
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} />
))}
</ul>
);
}
// ✅ Benar — pakai ID unik sebagai key
function DaftarTugas({ todos }) {
return (
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
Key yang digenerate di dalam render menyebabkan re-mount terus-menerus
Penyebab: Membuat ID baru menggunakan Math.random() atau Date.now() langsung di dalam .map() — setiap render menghasilkan key baru sehingga React selalu menganggap semua elemen baru dan me-mount ulang komponen dari awal.
Solusi:
// ❌ Jangan lakukan ini — key berubah setiap render
function DaftarDinamis({ items }) {
return (
<ul>
{items.map((item) => (
<li key={Math.random()}>{item.nama}</li>
))}
</ul>
);
}
// ✅ Generate ID saat data dibuat, simpan di state atau terima dari server
function DaftarDinamis() {
const [items, setItems] = useState([
{ id: crypto.randomUUID(), nama: "Item A" },
{ id: crypto.randomUUID(), nama: "Item B" },
]);
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.nama}</li>
))}
</ul>
);
}
Pertanyaan yang Sering Diajukan
Apa itu prop key di React dan mengapa wajib ada?
key adalah prop khusus yang digunakan React secara internal untuk mengidentifikasi setiap elemen dalam sebuah list. Ini bukan prop biasa — kamu tidak bisa mengaksesnya dari dalam komponen. React membutuhkannya untuk proses reconciliation agar bisa mendeteksi perubahan list secara efisien tanpa harus me-render ulang semua elemen dari awal.
Apakah key harus berupa angka atau boleh string?
key bisa berupa angka maupun string, yang penting nilainya unik di antara sibling dalam list yang sama. Dalam praktik, string ID dari database (seperti UUID atau ID auto-increment yang dikonversi ke string) adalah pilihan yang paling umum dan aman karena nilainya dijamin stabil.
Bagaimana cara menangani key jika data dari API tidak memiliki field id?
Jika API tidak mengembalikan ID, pertimbangkan beberapa opsi: minta backend untuk menambahkan field ID, kombinasikan beberapa field yang bersama-sama bersifat unik (misalnya key={`${item.username}-${item.tanggal}`}), atau tambahkan ID saat data pertama kali diterima menggunakan crypto.randomUUID(). Ini juga berkaitan dengan bagaimana kamu menyimpan data lokal — konsep yang mirip dibahas di artikel Mengelola Data Lokal dengan localStorage dan sessionStorage.
Apakah key berpengaruh pada performa aplikasi?
Ya, sangat berpengaruh. key yang tepat memungkinkan React melakukan update DOM secara minimal dan terarah. Tanpa key yang benar, React bisa melakukan re-render penuh pada seluruh list meskipun hanya satu item yang berubah. Pada aplikasi berskala besar — bayangkan feed produk dengan ratusan item — perbedaan performa ini bisa sangat signifikan dan terasa oleh pengguna.
Mengapa key tidak bisa diakses sebagai props di dalam komponen?
React secara sengaja tidak meneruskan key sebagai props karena key adalah instruksi untuk React sendiri, bukan untuk komponen. Jika kamu butuh nilai ID itu di dalam komponen, teruskan sebagai props terpisah:
// ✅ Teruskan id sebagai prop terpisah jika dibutuhkan di dalam komponen
{items.map((item) => (
<Item key={item.id} id={item.id} nama={item.nama} />
))}
Kesimpulan
Prop key mungkin terlihat seperti detail kecil, tapi ia adalah fondasi dari cara React mengelola list secara efisien. Ingat tiga aturan utama: gunakan ID yang stabil dan unik dari data, jangan gunakan indeks array kecuali untuk list yang benar-benar statis, dan selalu tempatkan key pada elemen paling luar dalam .map().
Dengan memahami konsep ini, kamu tidak hanya menghilangkan warning di console — kamu juga menulis React yang lebih cepat, lebih dapat diprediksi, dan lebih mudah di-debug. Selamat belajar dan terus semangat! Jika ada konsep React lain yang ingin kamu kuasai, jangan ragu untuk menjelajahi artikel-artikel lainnya di KamusNgoding — masih banyak topik seru yang menanti kamu.