Render danh sách và key trong React
Bài trước bạn đã biết state là bộ nhớ của component: đổi state thì React render lại. Nhưng dữ liệu thật hiếm khi chỉ có một giá trị, nó thường là một mảng công việc, một mảng sản phẩm, một mảng tin nhắn. Hôm nay bạn học cách biến mảng đó thành danh sách JSX, và cách chọn key để React cập nhật danh sách đúng.
Sau bài này, bạn sẽ:
- Render một mảng bằng
map()trong JSX mà không cần vòng lặp. - Giải thích được
keylà gì và React dùng nó để làm gì. - Chọn được
keyổn định từ dữ liệu thật (thường làid). - Nhận ra vì sao dùng index làm
keylại gây lỗi khó hiểu.
Từ danh sách HTML quen thuộc đến map trong JSX
Bạn đã viết thế này rất nhiều lần:
<ul>
<li>Táo</li>
<li>Cam</li>
<li>Xoài</li>
</ul>
Nhưng nếu danh sách đến từ một mảng, bạn không thể viết tay từng <li>. Trong JSX, dấu {} cho phép đặt biểu thức JavaScript, và map() là công cụ đúng để biến mảng dữ liệu thành mảng element:
const fruits = ["Táo", "Cam", "Xoài"];
function FruitList() {
return (
<ul>
{fruits.map((fruit) => (
<li key={fruit}>{fruit}</li>
))}
</ul>
);
}
Đọc theo kiểu đời thường: với mỗi quả trong giỏ, tạo một thẻ <li> dán nhãn tên quả đó. map() trả về một mảng element, và JSX render mảng đó thành nhiều phần tử liền nhau.
Nếu bạn viết {fruits.map((fruit) => { <li>{fruit}</li> })} thì sẽ nhận về mảng undefined, vì mũi tên có {} nghĩa là thân hàm, không tự trả về gì. Dùng () hoặc thêm return.
flowchart TD
Data["📦<br/>Mảng dữ liệu"] --> Map["🔁<br/>map()"]
Map --> Elements["🧩<br/>Mảng element"]
Elements --> React["⚛️<br/>React render"]
React --> Key["🔑<br/>So khớp theo key"]
Bài tập 1. Trong ứng dụng quản lý công việc, bạn có const tasks = [{ id: 1, title: "Học map" }, { id: 2, title: "Làm bài tập" }]. Hãy render thành danh sách <ul>.
Xem gợi ý
<ul>
{tasks.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
Chú ý hai điểm: dùng task.id cho key, và hiển thị task.title chứ không phải cả object (React không render object trực tiếp).
Key: tấm thẻ tên để React nhận diện từng phần tử
Khi danh sách thay đổi (thêm, xóa, sắp xếp lại), React cần biết phần tử nào giữ nguyên, phần tử nào mới, phần tử nào đã mất. Nó so khớp danh sách cũ với danh sách mới, và key chính là căn cứ so khớp đó. Hãy nghĩ tới phòng tập có tủ locker: nếu mọi tủ đều không ghi số, người giữ đồ phải mở từng tủ để đoán; có số tủ thì mọi thứ rõ ràng ngay.
Ba quy tắc cần nhớ:
keyphải duy nhất trong danh sách anh em cùng cấp, không cần duy nhất trong toàn app.keyphải ổn định: cùng một bản ghi thì cùng một key qua các lần render.keykhông phải prop: component con không đọc đượckeytrongprops. Nếu cần dùng giá trị đó bên trong, truyền thêm prop riêng nhưid={task.id}.
Điền nhanh để kiểm tra trí nhớ: React yêu cầu mỗi phần tử sinh ra từ mảng có một _____ duy nhất; với dữ liệu lấy từ API, nguồn key tốt nhất là _____ của bản ghi; và key không được truyền vào component con như một _____.
Chọn key ổn định từ dữ liệu thật
Thứ tự ưu tiên khi chọn key:
idtừ database hoặc từ API, tốt nhất.- Một trường tự nhiên duy nhất, ví dụ
emailhayslug. - Ghép nhiều trường nếu chưa có id, ví dụ
`${task.title}-${task.createdAt}`. - Index, chỉ dùng tạm và chỉ khi thật sự an toàn (xem phần sau).
Đừng dùng Math.random() hay Date.now(): mỗi lần render key lại đổi, React tưởng toàn bộ danh sách là mới, phá hủy rồi tạo lại các element, mất state bên trong và chậm hơn hẳn.
Một lỗi vị trí rất hay gặp: đặt key sai chỗ. key phải nằm trên element trực tiếp được trả về trong map, không phải trên thẻ bao ngoài, không phải bên trong component con.
// Sai: key nằm trên thẻ bọc ngoài, còn element trong map thì không có
{items.map((item) => (
<div key={item.id}>
<ItemRow />
</div>
))}
// Đúng: phần tử được map trực tiếp có key
{items.map((item) => (
<ItemRow key={item.id} item={item} />
))}
Bài tập 2. Danh sách chi tiêu của bạn chưa có id từ server, mỗi bản ghi có name, amount và createdAt. Bạn sẽ chọn key thế nào và vì sao?
Xem gợi ý
Ghép hai trường: key={${item.name}-${item.createdAt}}. Cách này ổn định qua các lần render và không phụ thuộc vị trí. Khi có id thật từ server, hãy chuyển sang dùng id. Tránh dùng amount một mình vì hai khoản chi có thể cùng số tiền.
Vì sao index làm key lại nguy hiểm
Dùng index làm key trông rất tiện: items.map((item, index) => <Row key={index} />). Vấn đề xuất hiện khi danh sách thay đổi thứ tự hoặc độ dài.
Hãy tưởng tượng một danh sách công việc, mỗi dòng có một ô input để ghi chú, và ô input đó giữ state riêng. Bạn xóa công việc đầu tiên. Với index làm key, phần tử ở vị trí 0 vẫn là "key 0", nên React giữ nguyên component ở vị trí 0 và chỉ thay props. Kết quả: ghi chú của công việc đã xóa lại "nhảy" sang công việc kế tiếp. Nhìn thì dữ liệu đúng mà trải nghiệm sai, đây là kiểu bug khiến người mới mất hàng giờ.
Tóm lại, khi dùng index làm key và _____ danh sách, React có thể gán nhầm state của phần tử cũ cho phần tử mới. Index chỉ tạm chấp nhận khi danh sách là _____, không đổi thứ tự, không thêm bớt và không có state bên trong.
Những cái bẫy khi render danh sách
- Thiếu
keythì React cảnh báo trong console, và cảnh báo này không nên bỏ qua. keybị trùng trong cùng một danh sách cũng gây cảnh báo và render sai.- Trả về
nullhoặcfalsecho phần tử không cần hiển thị thì hợp lệ, nhưng đừng quênreturnkhi dùngmapcó{}. - Đừng đặt
keylên cả hai chỗ (thẻ ngoài và component con), chỉ cần một chỗ đúng. - Render mảng chuỗi thì
keyvẫn cần, ví dụtags.map((tag) => <span key={tag}>{tag}</span>), miễn là chuỗi không trùng. - Muốn render theo điều kiện,
filter()trước rồi mớimap(), cách này dễ đọc hơn nhétifvào trong JSX.
Ôn nhanh bằng thẻ ghi nhớ
Front: Cách thường dùng để render một mảng dữ liệu thành danh sách trong JSX là gì?
Back: Gọi map() trên mảng và trả về JSX cho mỗi phần tử.
Front: map() trả về gì?
Back: Một mảng mới cùng độ dài, mỗi phần tử là kết quả của hàm callback.
Front: Vì sao React cảnh báo 'Each child in a list should have a unique key prop'?
Back: Vì các element sinh ra từ mảng đang thiếu prop key.
Front: key giúp React làm gì khi render lại?
Back: Nhận diện phần tử nào giữ nguyên, thêm mới hay bị xóa để cập nhật đúng chỗ.
Front: key phải duy nhất trong phạm vi nào?
Back: Trong danh sách anh em cùng cấp, không cần duy nhất toàn app.
Front: Component con có đọc được key qua props không?
Back: Không, React giữ key riêng và không truyền nó vào props.
Front: Nguồn key tốt nhất cho dữ liệu từ API là gì?
Back: id của bản ghi, vì ổn định và duy nhất.
Front: Vì sao không nên dùng index làm key khi danh sách có thể chèn, xóa hay sắp xếp lại? Back: Index đổi theo vị trí nên React có thể gán nhầm state/DOM của phần tử cũ cho phần tử mới.
Front: Khi nào dùng index làm key tạm chấp nhận được? Back: Danh sách tĩnh, không sắp xếp lại, không chèn/xóa và không có state bên trong.
Front: Vì sao không dùng Math.random() làm key?
Back: Key đổi mỗi lần render, React phá hủy và tạo lại toàn bộ phần tử.
Front: key phải đặt ở đâu?
Back: Trên element trực tiếp được trả về trong map, không phải thẻ bao ngoài.
Front: Trong map dùng mũi tên có {} thì cần nhớ gì?
Back: Phải return JSX, nếu không sẽ nhận về mảng undefined.
Front: Hai phần tử trong cùng danh sách có key trùng nhau thì sao? Back: React cảnh báo và có thể render hoặc cập nhật sai phần tử.
Mang gì sang bài checkpoint
- Danh sách trong JSX =
map()+ mộtkeyổn định cho mỗi phần tử. keylà định danh nội bộ của React, không phải prop, và chỉ cần duy nhất trong danh sách anh em.- Ưu tiên
idtừ dữ liệu; index chỉ dùng cho danh sách tĩnh, an toàn. - Thay đổi thứ tự hay độ dài danh sách là lúc lỗi index-key lộ ra rõ nhất.
Bài tiếp theo là checkpoint của module: bạn sẽ ghép JSX, props, state, xử lý sự kiện và render danh sách có key thành một màn hình quản lý hoàn chỉnh.