Jika Anda pernah menghabiskan waktu di React, Anda pasti pernah melihat peringatan kuning di konsol Anda: “Each child in a list should have a unique ‘key’ prop.” Kedengarannya seperti saran yang sopan, tetapi React sebenarnya memperingatkan bahwa ia tidak dapat membedakan item-item dalam daftar Anda. Abaikan saja, dan Anda pada akhirnya akan merilis bug yang sangat sulit untuk direproduksi—state yang melompat ke baris yang salah, input teks yang kehilangan fokus, atau animasi yang berjalan pada elemen yang salah.
Mesin rendering React tidak membandingkan UI Anda piksel demi piksel. Ia membangun pohon objek ringan yang disebut virtual DOM, membandingkan pohon baru dengan pohon sebelumnya, dan menghitung set perubahan terkecil yang diperlukan untuk DOM asli. Saat Anda merender sebuah daftar, React melihat sebuah array dari elemen-elemen bersaudara (sibling elements). Tanpa key, ia tidak memiliki cara yang andal untuk mengetahui apakah sebuah item berpindah, diganti, atau dihapus. Secara default, ia akan mencocokkan berdasarkan posisi, yang mana sangat rapuh. Key bertindak sebagai identitas yang stabil. Key memberi tahu React, “Elemen ini adalah elemen yang sama seperti sebelumnya, meskipun sekarang berada di slot yang berbeda.” Jika Anda melakukan kesalahan di sini, Anda menukar pembaruan yang deterministik dengan tebakan semata.
Perbaikan Minimum
Peringatan ini biasanya muncul di dalam pemanggilan map. Anda harus menetapkan nilai unik ke atribut key pada elemen tingkat atas yang dikembalikan dari iterator.
Berikut adalah pola yang Anda lihat di setiap codebase yang memicu peringatan tersebut:
const UserList = ({ users }) => {
return (
<ul>
{users.map((user) => (
<li>{user.name}</li>
))}
</ul>
);
};
React melihat tiga tag <li dan tidak tahu mana yang mana. Perbaikannya hanya satu atribut:
const UserList = ({ users }) => {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
key harus ditetapkan ke elemen secara langsung di dalam callback map. Jika Anda mengekstrak <li ke dalam komponen UserItem yang terpisah, key tersebut tetap harus berada pada komponen di lokasi pemanggilan (call site):
{users.map((user) => (
<UserItem key={user.id} user={user} />
))}
Menempatkan key di dalam UserItem pada <div> internalnya tidak akan menghilangkan peringatan tersebut dan tidak akan memperbaiki perilaku rekonsiliasi (reconciliation). React mencari key pada elemen yang dikembalikan oleh iterator.
Mengapa Menggunakan Index sebagai Key Itu Berbahaya
Sangat menggoda untuk menghilangkan peringatan tersebut dengan menggunakan argumen kedua dari map:
{users.map((user, index) => (
<li key={index}>{user.name}</li>
))}
Ini menghilangkan gangguan di konsol, tetapi tidak menyelesaikan masalah mendasar. Indeks array bukanlah identitas. Mereka adalah posisi, dan posisi dapat berubah.
Bayangkan sebuah daftar berisi tiga pengguna yang dirender dalam urutan ini:
- Alice (indeks 0)
- Bob (indeks 1)
- Charlie (indeks 2)
Jika Anda menghapus Alice, Bob bergeser ke indeks 0 dan Charlie bergeser ke indeks 1. React membandingkan pohon baru dengan pohon lama. Ia melihat bahwa indeks 0 sekarang berisi data Bob, sehingga ia mengubah (mutate) node DOM yang ada yang sebelumnya menampilkan Alice. Jika node tersebut sedang dalam fokus, kursor akan tetap berada di baris pertama, tetapi teksnya berubah menjadi Bob. Jika baris tersebut berisi <input> dengan state lokal, state tersebut akan tetap tertahan di indeks 0. Pengguna mengetik ke dalam apa yang terlihat seperti baris Bob, padahal state tersebut milik Alice. Kekacauan yang sama terjadi saat Anda mengurutkan (sort), menyaring (filter), atau menambahkan item di awal (prepend). Satu-satunya tempat yang aman untuk menggunakan indeks sebagai key adalah daftar yang benar-benar statis: tidak ada pengurutan ulang, tidak ada penyaringan, tidak ada penyisipan, dan tidak ada penghapusan. Tautan navigasi yang di-hardcode dan tidak pernah berubah adalah contoh yang tepat. Segala hal lainnya membutuhkan pengidentifikasi yang nyata.
Di Mana Menemukan Key yang Stabil
Key terbaik adalah pengidentifikasi unik yang sudah ada dalam model data Anda. Primary key database seperti id sangat ideal karena dijamin unik dan tetap ada di berbagai render. Jika backend Anda mengembalikan objek dengan uuid, slug, atau bidang unik alami lainnya, gunakanlah itu.
Ketika respons API Anda tidak memiliki bidang unik apa pun, Anda memiliki dua jalur praktis. Pertama, bicaralah dengan tim backend Anda dan mintalah mereka untuk menyertakan id. Mengirimkan data relasional tanpa primary key adalah sebuah "code smell", dan memperbaikinya di sumbernya akan menghilangkan ambiguitas di seluruh stack Anda. Kedua, jika Anda membuat item sepenuhnya di sisi klien—misalnya, daftar todo di mana pengguna membuat tugas sebelum data dikirim ke server—buatlah ID sekali saja pada saat pembuatan. Library seperti uuid atau nanoid dibuat khusus untuk hal ini. Buatlah ID saat pengguna mengirimkan formulir, simpan di dalam objek, dan gunakan sebagai key selamanya.
Jangan pernah membuat key di dalam jalur render (render path). Memanggil Math.random() atau Date.now() selama render komponen akan menghasilkan nilai baru pada setiap iterasi. React akan melihat key baru, menganggapnya sebagai elemen yang benar-benar baru, menghancurkan node DOM lama, dan membuat yang baru. Semua state di dalam elemen tersebut akan ter-reset. Fokus akan hilang. Performa akan anjlok karena React melakukan pekerjaan DOM yang tidak perlu. Key yang dibuat secara acak justru lebih buruk daripada tidak menggunakan key sama sekali.
Fragments, Komponen, dan Scope
Jebakan yang kurang terlihat melibatkan React Fragments. Jika Anda melakukan mapping pada data dan perlu mengembalikan beberapa elemen sibling tanpa pembungkus <div>, Anda mungkin akan menggunakan sintaks singkat:
{items.map((item) => (
<>
<dt>{item.term}</dt>
<dd>{item.definition}</dd>
</>
))}
Sintaks singkat <>...</> tidak mendukung props, yang berarti Anda tidak dapat menyematkan key. Dalam hal ini, beralihlah ke sintaks eksplisit yang lengkap:
{items.map((item) => (
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.definition}</dd>
</React.Fragment>
))}
React membutuhkan key tersebut pada Fragment agar dapat melacak pasangan tersebut sebagai satu unit tunggal di seluruh render.
Poin halus lainnya: key bukanlah props dalam pengertian biasa. Jika Anda menulis <ListItem key={item.id} />, komponen ListItem tidak dapat membaca props.key. React mengonsumsinya secara internal untuk pembukuan. Jika komponen Anda benar-benar membutuhkan pengidentifikasi tersebut untuk logikanya sendiri, kirimkan secara terpisah dengan nama lain, seperti itemId.
Aturan Praktis agar Anda Tetap Aman
- Utamakan ID database. ID tersebut unik, berbasis angka atau string, dan stabil.
- Gunakan
uuidataunanoiduntuk data yang hanya ada di sisi client. Hasilkan ID satu kali saat record dibuat, bukan di dalam render komponen. - Jangan pernah menurunkan key dari indeks array jika daftar tersebut dapat berubah. Pengurutan, penyaringan, dan penghapusan akan menimbulkan bug visual dan state.
- Jangan pernah menggunakan
Math.random(),Date.now(), atau nilai apa pun yang berubah di antara render. Ini akan memaksa unmounting dan remounting yang tidak perlu. - Ingat bahwa Fragments membutuhkan bentuk panjang jika berada di dalam
mapdan memerlukan key. - Letakkan key pada elemen yang dikembalikan oleh
map, bukan di dalam komponen anak.
Kesimpulan Utama
Prop key bukanlah sekadar aturan lint dekoratif. Ini adalah cara React menjaga identitas di seluruh render. Anggaplah seperti primary key dalam tabel database. Ketika identitas tersebut stabil, React dapat memindahkan, memperbarui, dan menghapus elemen dengan presisi. Ketika identitas tersebut hilang atau tidak stabil, Anda akan menanggung risikonya dengan state UI yang rusak dan rekonsiliasi yang lambat. Perbaiki sekali saja di lapisan data, dan daftar Anda akan berperilaku secara terprediksi tidak peduli seberapa banyak mereka tumbuh atau berubah.
