Chắc hẳn trong quá trình làm việc với Vuejs, mọi người không ít lần sử dụng v-for để xử lý các vòng lặp. Và có hai vấn đề mà khá nhiều người đang gặp phải khi set giá trị cho key trong v-for.
Trong docs của Vue có đề cập: Children of the same common parent must have unique keys
.
Nhiều người đang hiểu same parent
ở đây có nghĩa là nằm trong cùng component, hay cùng một file .vue
. Tuy nhiên, Vue chỉ yêu cầu các key khác nhau khi trong cùng một v-for , có nghĩa là việc hai element có cùng key trong cùng một file .vue
là bình thường, không có vấn đề gì.
Chỉ trừ duy nhất một trường hợp tuy nằm ở các v-for khác nhau nhưng key vẫn phải khác nhau đó là khi các v-for đó siblings - nghĩa là chúng nằm cạnh nhau trong DOM
, ví dụ:
<div v-for="(item, index) in items" :key="index">{{ item.text }}</div>
<div v-for="(product, index) in products" :key="index">{{ product.name }}</div>
Với case này, chúng ta có 2 cách xử lý:
- Thay đổi format cho key, mà thông thường mọi người vẫn hay dùng nhất là thêm prefix cho các key để phân biệt chúng với nhau (kiểu:
item-${index}
product-${index}
) - Wrap từng v-for này vào các thẻ
div
Việc sử dụng index làm key cho v-for (như ví dụ ở trên) về cơ bản là "thừa" (mặc dù nó vẫn có thể giúp chúng ta qua được cảnh báo của Eslint), lý do là vì trong cơ chế mặc định v-for của Vue cũng đã sử dụng index để theo dõi sự thay đổi của v-for.
Lạm dụng index trong mọi trường hợp sẽ có thể gây ra cho chúng ta nhiều tình huống kỳ quặc và rất khó để debug.
Một ví dụ minh họa:
Chúng ta có một component TodoItem
như sau:
<template>
<div>
<input type="checkbox" :id="item.id" v-model="isDone" />
<label :for="item.id">{{ item.name }}</label>
</div>
</template>
<script>
export default {
props: {
item: {
type: Object,
required: true,
},
},
data() {
return {
isDone: false,
};
},
};
</script>
Component này sẽ có nhận một prop là item, hiển thị name của item lên giao diện, và bên cạnh là một checkbox được reactive với isDone
được khai báo trong data (với giá trị mặc định ban đầu là false
)
Ở page bằng cách dùng một vòng v-for, ta đẩy từng phần tử của mảng vào component thông qua prop item
. Ngoài ra chúng ta sẽ dùng index là key cho từng phần tử:
<todo-item v-for="todo in todos" :item="todo" :key="index"></todo-item>
<button @click="addNew()">Add new item</button>
Trong đó todos
là một mảng các giá trị được khởi tạo ở data của page, và addNew
là một method với nhiệm vụ là là thêm một giá trị mới vào đầu của mảng todos
bằng method unshift
:
data() {
return {
todos: [
{ id: 1, name: "Do the homework" },
{ id: 2, name: "Read a book" },
{ id: 3, name: "Clean the house" },
],
}
},
methods: {
addNew() {
const newItem = { id: 4, name: "Buy food" };
this.todos.unshift(newItem);
}
}
Chúng ta sẽ được một giao diện như này:
Sau đó, chúng ta thực hiện tích vào các checkbox của list:
Tiếp theo, chúng ta thêm item mới bằng cách nhấn vào button Add new
, kỳ vọng của hành động này sẽ là thêm mới item: Buy food
lên đầu danh sách với ô checkbox ở trạng thái là uncheck. Tuy nhiên khi click vào button Add new
thì kết quả chúng ta nhận được lại là:
Nguyên nhân chính là do cơ chế Tối ưu hóa các thay đổi với DOM và sử dụng lại các phần tử hiện có nhiều nhất có thể (hay còn gọi là cơ chế in-place patch
).
Item mới Buy food
được thêm vào đúng vị trí của item Do the homework
, vì vậy thay vì một phần tử mới được tạo ra, Vue đã thực hiện "vá" nó bằng cập nhật lại nội dung của phần tử từ Do the homework
thành Buy food
. Đồng thời giữ nguyên giá trị isDone
là true
như ở trạng thái cũ.
Trong khi đó phần tử Clean the house
bị "đẩy" xuống cuối cùng - một vị trí mà trước đây chưa từng tồn tại trong DOM. Do đó, component TodoItem
được khởi tạo lại với giá trị isDone
là false
.
Để tránh xảy ra những tình huống như trên, chúng ta nên sử dụng ID để set key trong mọi trường hợp có thể, ví dụ:
<todo-item v-for="todo in todos" :item="todo" :key="todo.id"></todo-item>
Vì id
là duy nhất cho mỗi item, chúng ta đang "hiển thị" một cách hiệu quả thuật toán những mục nào đã có trước khi thay đổi và mục nào đã được thêm vào.
Nhiều người hỏi rằng họ luôn luôn phải tạo id
?
Đúng là đôi khi việc tạo id mới là thừa và có thể tránh được.
Sau đây là một số điều kiện:
- Danh sách và các mục là static – chúng không được tính toán và không thay đổi.
- Các mục trong danh sách không có
id
- Danh sách không bao giờ được lọc hoặc sắp xếp lại.
Khi nào tất cả các điều kiện trên được đáp ứng, bạn có thể sử dụng index làm key một cách an toàn.
- https://vuejs.org/v2/guide/list.html
- https://robinpokorny.medium.com/index-as-a-key-is-an-anti-pattern-e0349aece318
- https://stackoverflow.com/questions/44531510/why-not-always-use-the-index-as-the-key-in-a-vue-js-for-loop
- https://forum.vuejs.org/t/v-for-key-does-the-key-need-to-be-unique-across-the-entire-component/28040/5