Menu

List và key trong React: map(), prop key, key từ index

Để render một danh sách trong React, bạn gọi map() trên mảng và trả về JSX cho từng phần tử, kèm một prop key cho mỗi phần tử. Tìm hiểu filter rồi map, key nên lấy từ đâu và vì sao key từ index làm lẫn lộn phần tử khi danh sách thay đổi.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Để render một danh sách trong React, hãy gọi map() trên mảng và trả về một đoạn JSX cho từng phần tử. Gắn cho phần tử ngoài cùng của mỗi mục một prop key có giá trị nhận diện mục đó, thường là ID của nó, để React phân biệt được các mục khi danh sách thay đổi.

Thêm { id: 'dat', name: 'Date', price: 4 } vào mảng và một dòng thứ tư sẽ xuất hiện. JSX không có cú pháp vòng lặp riêng: map trả về một mảng các element, và React render một mảng bằng cách render lần lượt từng element.

Render mảng bằng map

map gọi hàm của bạn một lần cho mỗi phần tử và gom kết quả vào một mảng mới. Bên trong cặp ngoặc nhọn của JSX, mảng đó chỉ là một giá trị, nên bạn có thể map ngay trong markup như trên, hoặc dựng mảng trước:

const items = fruits.map((fruit) => <li key={fruit.id}>{fruit.name}</li>);
return <ul>{items}</ul>;

Có hai chi tiết hay khiến người ta vấp. Nếu dùng ngoặc nhọn trong arrow function, bạn cần return: fruits.map((f) => { return <li key={f.id}>{f.name}</li>; }). Thiếu nó, hàm trả về undefined và danh sách trống trơn. Ngoài ra, key đặt trên element được trả về từ map, không phải trên một element nằm bên trong nó. Nếu mục là một component, hãy đặt key lên component: <FruitRow key={fruit.id} fruit={fruit} />.

Filter rồi map

Để chỉ hiển thị một số mục, hãy lọc mảng trước rồi map kết quả. Mỗi phương thức làm một việc, và chuỗi lệnh đọc lên giống như câu "những loại quả khớp điều kiện, dưới dạng các mục danh sách".

Gõ er vào ô tìm kiếm, danh sách thu hẹp còn Eraser, Ruler và Stapler; tích vào In stock only thì Ruler cũng biến mất. Danh sách đã lọc được tính trong lúc render từ hai state, nên không cần một mảng thứ hai trong state phải đồng bộ. visible.length === 0 && là cách dùng && an toàn vì phép so sánh cho ra một boolean thật (trang render có điều kiện cho thấy chuyện gì xảy ra với một số 0 đứng trơ trọi).

Key làm gì

Khi một danh sách render lại, React so sánh mảng element mới với mảng trước đó. Key là cách nó ghép cặp: element có key 3 bây giờ chính là element có key 3 lúc trước, kể cả khi nó đã di chuyển. React giữ lại DOM node và state component của mục đó, chỉ di chuyển hoặc cập nhật những gì thay đổi. Mục có key mới sẽ được tạo, còn key đã biến mất thì bị gỡ bỏ.

Không có key, thứ duy nhất React có thể dựa vào để ghép cặp là vị trí. Điều đó ổn khi không có gì di chuyển, và sai ngay khi có thứ gì đó di chuyển.

Vì sao key từ index gây lỗi

map truyền index làm đối số thứ hai, nên key={index} rất hấp dẫn. Nhưng index mô tả một vị trí, không phải một mục. Chèn một mục lên đầu, index của mọi mục đều dịch đi một, nên React tưởng mục 0 vẫn là mục 0 và giữ nguyên state cũ ở đó.

Bản demo dưới đây render cùng một danh sách hai lần, một lần dùng key là index và một lần dùng ID. Mỗi hàng có một ô input tự giữ chữ của nó.

Gõ math cạnh Ada ở cả hai danh sách, rồi nhấn Add to top. Trong danh sách dùng ID, ghi chú vẫn ở cạnh Ada. Trong danh sách dùng index, ghi chú nhảy sang người mới, vì ô input đầu tiên vẫn là ô ở vị trí 0, và vị trí 0 giờ là một người khác. Tên ở cả hai danh sách đều đúng, vì chúng lấy từ dữ liệu; chỉ phần state nằm trong DOM và trong component bị sai. Ứng dụng thực tế gặp lỗi này với checkbox, focus, animation và mọi component có useState.

Key từ index chỉ an toàn khi thỏa mãn tất cả các điều kiện sau: danh sách không bao giờ bị đổi thứ tự hay lọc, các mục không bao giờ bị chèn hay xóa ngoại trừ ở cuối, và các mục không có state riêng. Một danh sách link cố định ở footer là đủ điều kiện. Thứ gì người dùng có thể chỉnh sửa thì thường là không.

Key lấy từ đâu

Key tốt nhất là một ID có sẵn trong dữ liệu: ID trong cơ sở dữ liệu, mã SKU sản phẩm, slug, tên người dùng. Dữ liệu từ máy chủ gần như luôn có sẵn một ID như vậy.

Với các mục tạo ra trong trình duyệt, hãy gán ID cho mỗi mục lúc tạo ra nó và lưu cùng với mục đó: một bộ đếm như trong demo trên, hoặc crypto.randomUUID(). Không bao giờ tạo key trong lúc render:

// Wrong: a new key on every render, so React remounts every item every time
{todos.map((todo) => <Todo key={Math.random()} todo={todo} />)}

// Right: the ID is created once, in the event that adds the item
function addTodo(text) {
    setTodos([...todos, { id: crypto.randomUUID(), text }]);
}

Một key thay đổi sau mỗi lần render còn tệ hơn index: mọi mục bị hủy và tạo lại mỗi lần, ô input mất focus khi bạn đang gõ, và chẳng có gì giữ được state. Chính mẹo này lại hữu ích khi dùng có chủ đích: đổi key của một component sẽ đặt lại component đó, cách mà trang useState dùng để xóa trắng một form.

Key chỉ cần duy nhất giữa các phần tử anh em

Một key chỉ cần là duy nhất trong một danh sách, không phải trong toàn bộ ứng dụng. Hai danh sách riêng biệt có thể dùng lại cùng các ID, vì React chỉ so sánh key giữa các phần tử con của cùng một cha.

Cả hai phần đều có món ăn với key 1 và 2, và điều đó hoàn toàn đúng: mỗi ul là một danh sách riêng. Key trùng nhau trong cùng một danh sách mới là vấn đề: React không phân biệt được các mục bị trùng, và khi danh sách cập nhật, nó có thể nhân đôi hoặc làm mất chúng.

Khi một mục render ra nhiều element anh em, chẳng hạn một <dt> và một <dd>, hãy bọc chúng trong <Fragment key={item.id}>. Cú pháp rút gọn <> không nhận key.

Cảnh báo "unique key prop"

Nếu bạn bỏ quên key, React in dòng này ra console của trình duyệt trong lúc phát triển:

Each child in a list should have a unique "key" prop.

Check the render method of `App`.

Dòng thứ hai nêu tên component có lần render tạo ra danh sách, cho bạn biết cần sửa map nào. Hai phần tử anh em trùng key sẽ nhận một thông báo khác, bắt đầu bằng "Encountered two children with the same key". Bản xem trước trên trang này chạy giống bản build production, vốn bỏ qua bước kiểm tra đó, nên bạn sẽ không thấy cảnh báo ở đây kể cả khi xóa một key. Dù vậy, hãy thử xóa một key trong ví dụ đầu tiên: danh sách vẫn render, vì thiếu key là rủi ro về tính đúng đắn, không làm ứng dụng sập. Để sửa cảnh báo, hãy thêm một key ổn định vào element trả về từ map; thêm key={index} làm cảnh báo im lặng nhưng mang lỗi ở trên quay trở lại.

Cũng lưu ý rằng key không được truyền vào component của bạn. Bên trong FruitRow, props.key là undefined. Nếu component cần ID, hãy truyền nó thêm lần nữa như một prop bình thường: <FruitRow key={f.id} id={f.id} />.

Câu hỏi thường gặp

Vì sao React cần key cho các phần tử trong danh sách?

Key cho React biết phần tử nào là phần tử nào giữa các lần render. Khi mảng thay đổi, React ghép phần tử cũ với phần tử mới theo key, nhờ vậy nó giữ được state và DOM node của từng phần tử đi cùng đúng dữ liệu, thay vì đoán theo vị trí.

Có thể dùng index của mảng làm key không?

Chỉ với danh sách không bao giờ đổi thứ tự và không bao giờ chèn hay xóa phần tử ở giữa. Nếu phần tử có thể di chuyển, key từ index gắn state với vị trí, nên chữ đã gõ, checkbox và focus sẽ rơi vào nhầm phần tử.

Nên dùng gì làm key?

Một ID thuộc về dữ liệu: ID trong cơ sở dữ liệu, mã SKU sản phẩm, tên người dùng. Với phần tử tạo ra trong trình duyệt, hãy tạo ID một lần lúc tạo phần tử (một bộ đếm hoặc crypto.randomUUID()), không bao giờ tạo trong lúc render.

Key có phải là duy nhất trong toàn bộ ứng dụng không?

Không. Key chỉ cần là duy nhất giữa các phần tử anh em trong cùng một danh sách. Hai danh sách khác nhau có thể dùng chung key.

Component có đọc được key của chính nó không?

Không. key do React dùng và không được truyền vào component như một prop. Nếu component cần ID, hãy truyền thêm lần nữa dưới tên khác, chẳng hạn id={item.id}.

Sửa cảnh báo "Each child in a list should have a unique key prop" như thế nào?

Thêm key vào phần tử ngoài cùng mà bạn trả về từ map, dùng một ID ổn định của phần tử. Nếu phần tử đó là fragment, hãy dùng <Fragment key={id}>, vì <> không nhận key.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu