Lỗi Gặp Phải
Bạn mở console trình duyệt và thấy cảnh báo này:
Warning: Each child in a list should have a unique "key" prop.
Check the render method of `UserList`. See https://reactjs.org/link/warning-keys for more information.
at li
at UserList
React hiển thị cảnh báo này khi bạn dùng map trên một mảng mà quên thêm prop key — hoặc khi các key bạn dùng không thực sự là duy nhất trong toàn bộ danh sách.
Tại Sao Điều Này Quan Trọng
React dựa vào key để theo dõi phần tử nào đã thay đổi, di chuyển, hoặc bị xóa giữa các lần render. Không có key duy nhất, thuật toán diffing của nó phải đoán — và đoán sai.
Đây là một tình huống cụ thể: bạn có danh sách todo với 5 mục. Bạn xóa mục số 2. Với index key, mục số 3 giờ chiếm vị trí index 1. React nghĩ đây là cùng một component với mục số 2 cũ và bỏ qua việc cập nhật nó. Kết quả? Checkbox được tích ở các mục sai, hoặc ô input hiển thị giá trị cũ.
Cụ thể, key không đúng gây ra:
- State component sai sau khi sắp xếp lại hoặc xóa
- Re-render bất ngờ hoặc bỏ sót cập nhật
- Ô input giữ giá trị cũ sau khi danh sách thay đổi
Đây là cảnh báo, không phải lỗi crash. Nhưng nếu bỏ qua đủ lâu, bạn sẽ mất hàng giờ để truy tìm những bug có vẻ không thể tái hiện.
Nguyên Nhân Thường Gặp
- Thiếu prop
keyhoàn toàn trên các phần tử trong danh sách - Dùng index của mảng làm key khi danh sách có thể được sắp xếp lại hoặc lọc
- Dữ liệu có ID trùng lặp
- Key đặt sai phần tử — đặt trên fragment bọc ngoài thay vì phần tử ngoài cùng được trả về
Cách Sửa Từng Bước
1. Thêm key cho mỗi phần tử trong danh sách
Trường hợp cơ bản nhất — bạn đang map qua một mảng mà không có key nào cả:
// ❌ Thiếu key
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li>{user.name}</li> // Cảnh báo xuất hiện ở đây
))}
</ul>
);
}
// ✅ Thêm key dùng ID duy nhất và ổn định
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
2. Ngừng dùng index của mảng làm key
Index key trông có vẻ vô hại. Nó tắt cảnh báo. Nhưng rắc rối bắt đầu ngay khi người dùng sắp xếp, lọc, hoặc xóa một mục nào đó:
// ❌ Index key — bị lỗi khi sắp xếp lại/xóa
{items.map((item, index) => (
<TodoItem key={index} item={item} />
))}
// ✅ Dùng ID ổn định từ dữ liệu của bạn
{items.map(item => (
<TodoItem key={item.id} item={item} />
))}
Index key chỉ an toàn cho danh sách thực sự tĩnh — những thứ không bao giờ sắp xếp lại, lọc, hay thu nhỏ. Mọi trường hợp khác đều cần ID thực sự.
Tạo ID khi dữ liệu được khởi tạo (ví dụ, dùng crypto.randomUUID() hoặc nanoid()), không phải trong lúc render. Chi tiết hơn ở bước 5.
3. Key trên fragment
Trả về nhiều phần tử cho mỗi mục? Bạn cần dùng Fragment với key. Cú pháp viết tắt <></> không chấp nhận props, nên bạn phải dùng <Fragment> được import tường minh:
import { Fragment } from 'react';
// ❌ Fragment viết tắt không thể nhận key
{items.map(item => (
<>
<dt>{item.term}</dt>
<dd>{item.definition}</dd>
</>
))}
// ✅ Fragment tường minh với key
{items.map(item => (
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.definition}</dd>
</Fragment>
))}
4. Danh sách lồng nhau
Mỗi cấp của danh sách đều cần key — không chỉ cấp ngoài cùng:
// ❌ Các phần tử danh sách bên trong thiếu key
{categories.map(cat => (
<div key={cat.id}>
<h3>{cat.name}</h3>
<ul>
{cat.items.map(item => (
<li>{item.label}</li> // ← Cảnh báo ở đây cũng vậy
))}
</ul>
</div>
))}
// ✅ Key trên mọi phần tử được map
{categories.map(cat => (
<div key={cat.id}>
<h3>{cat.name}</h3>
<ul>
{cat.items.map(item => (
<li key={item.id}>{item.label}</li>
))}
</ul>
</div>
))}
5. Khi bạn thực sự không có ID
Một số danh sách thực sự là tĩnh — một tập hợp cố định tên ngôn ngữ, nhãn trạng thái, hoặc tùy chọn cấu hình không bao giờ sắp xếp lại và không bao giờ lặp lại. Trong trường hợp đó, bản thân giá trị là một key hợp lệ:
const SUPPORTED_LANGS = ['JavaScript', 'TypeScript', 'Python', 'Go'];
{SUPPORTED_LANGS.map(lang => (
<li key={lang}>{lang}</li>
))}
Dữ liệu động là câu chuyện khác. Gắn ID vào nguồn dữ liệu — trước khi dữ liệu đến được component. Cách nhanh với nanoid:
import { nanoid } from 'nanoid';
const items = rawData.map(item => ({ ...item, id: nanoid() }));
// Giờ dùng item.id làm key
Làm điều này một lần, bên ngoài component. Không bao giờ gọi nanoid() hoặc Math.random() bên trong .map() trong lúc render — mỗi lần render sẽ tạo ra một key mới, buộc React phải unmount và remount lại mọi phần tử.
Kiểm Tra Sau Khi Sửa
- Mở Chrome DevTools → tab Console
- Tải lại trang — cảnh báo về key không còn xuất hiện nữa
- Kiểm tra kỹ danh sách: thêm mục, xóa mục, sắp xếp lại nếu có thể
- Kiểm tra rằng các ô input bên trong phần tử danh sách giữ đúng giá trị sau mỗi thao tác
- Trong React DevTools (tab Components), kiểm tra các phần tử danh sách — key là nội bộ và sẽ không hiển thị là props, nhưng cảnh báo trong console sẽ im lặng
Danh Sách Kiểm Tra Nhanh
- Mỗi callback
.map()trả về một phần tử có propkey - Key lấy từ ID dữ liệu ổn định, không phải index mảng (trừ khi danh sách thực sự tĩnh và không bao giờ thay đổi)
- Key là duy nhất trong các phần tử cùng cấp — không cần phải duy nhất toàn cục
- Fragment có key dùng
<Fragment key={...}>, không dùng<></> - Danh sách lồng nhau có key ở mọi cấp
- Key không bao giờ được tạo trong lúc render — không
Math.random(), khôngDate.now(), khôngnanoid()nội tuyến bên trong.map()

