v-for
ディレクティブを使うと、データに納められた複数の項目がページに要素として差し込めます。このときv-for
にkey
を与えるのが、「スタイルガイド」に定められた「必須」のルールです(「キー付き v-for」)。
一意のkey特別属性とインデックス
key
特別属性は、要素に一意の値を与えます。:key
はv-bind:key
の省略構文です(「key」参照)。
<ul>
<li v-for="item in items" :key="item.id">...</li>
</ul>
また、配列からv-for
でデータを取り出すとき、第2引数に配列インデックスが受け取れます(「v-for で配列に要素をマッピングする」参照)。インデックスはもちろん一意です。
<ul id="example">
<li v-for="(item, index) in items">
{{parentMessage}} - {{index}} - {{item.message}}
</li>
</ul>
では、v-for
で配列インデックスを得て、key
に与えればよいのではないでしょうか。結論からいいますと、配列インデックスはkey
の値とすべきではありません。
key
を与えない場合は、配列インデックスが使われるとのことです。
また、要素の追加や削除あるいは順序変更がない場合にかぎれば、
key
を使わなくても問題はないようです。むしろ「標準のモードは効率がいい」とされます。ただ、あとで変わりやすい前提と思われるので、key
を「必須」にしているのではないかと推測されます。
インデックスをキーにすると要素のアニメーションが意図しない動きになる
v-for
で得た配列インデックスをkey
の値に与えると、要素のアニメーションが意図しない動きになったりします。「Vue.js: 動的に変更する要素にアニメーションを加える」でつくったTodoリストは、項目を加えたり除いたりすると水平アニメーションします(サンプル001)。
サンプル001■Vue.js + ES6: TodoMVC with animation
See the Pen Vue.js + ES6: TodoMVC with animation by Fumio Nonaka (@FumioNonaka) on CodePen.
<li v-for="todo in filteredTodos"
:key="todo.id"
>
</li>
データをはじめに読み込んだとき(todoStorage.fetch())は、配列インデックスを項目のkey
に与え、配列の長さを新たに加える項目の値(todoStorage.uid)としています。そのうえで、項目を加える(addTodo())たびに値を加算するのです。key
の値は一意であるとともに、ひとたびつけられた項目の値は決して変わりません。
const todoStorage = {
fetch() {
todos.forEach((todo, index) =>
todo.id = index
);
todoStorage.uid = todos.length;
return todos;
},
};
const app = new Vue({
methods: {
addTodo() {
this.todos.push({
id: todoStorage.uid++,
title: value,
});
},
},
});
このテンプレートをつぎのように書き替え、配列インデックスがkey
の値となるようにします(サンプル002)。
<li v-for="(todo, index) in filteredTodos"
:key="index"
>
</li>
サンプル002■Vue.js + ES6: Setting the index of item to its :key
See the Pen Vue.js + ES6: Setting the index of item to its :key by Fumio Nonaka (@FumioNonaka) on CodePen.
すると、なんということでしょう。どの項目を削除してもアニメーションするのは最後の項目になってしまいます。
図001■どの項目を削除しても最後の項目がアニメーションする
keyは振り直してはいけない
一意のkey
は、どのデータが変わったのか、Vueが追いかけるために用いられます。ところが、配列インデックスを使ってしまうと、前の要素が除かれたら後の番号はつけ変えなければなりません。すると、Vueはどの要素のデータが変わったのか後追いできなくなります。つまり、key
は振り直してはいけないのです。
それでも、データの削除そのものは正しく行われます。けれども、アニメーションが求められると、要素をすぐに消すわけにはいきません。配列インデックスが使われた場合、Vueはデータとの対応は見ず、最後の要素を削除するようです(「キー付き v-for」の「詳細な説明」参照)。そのため、つねに最後の要素がアニメーションします。もちろん、データは正しく削除されていますので、新たなインデックスにしたがって要素のデータとkey
は振り直されるというのが、前掲サンプル002の動きと考えればよいでしょう(図001)。
For example, let's say the first item in the array is removed. if you use an index, Vue will assume the last element was removed and update every remaining element as it shifts everything down, thinking they've all changed.
(「Style Guide: Keyed v-for (question)」より)
たとえば、配列の最初の項目が除かれたとしましょう。もしインデックスを使っていたら、Vueは最後の要素が削除されたとみなして、残った配列データで要素を順に更新し、すべてが変更されることになります。(筆者訳)
前掲サンプル001は、もとデータの配列の長さをつぎのkey
の値とし、項目が加わるたびに加算しました。一意であるとともに、変わらぬ値が振られるので確実です。もっとも、key
の値を画面に表示したとき、追加・削除を繰り返すと、番号の開きが大きくなるのは気になるかもしれません。その場合は「追加と削除が繰り返される配列要素のオブジェクトに一意のid番号を振る」をお読みください。
[追記: 2019年09月03日] 関連でつまずいた問題を、覚書として加えます。データの配列が空(length
が0)になったとき、続けてkey
属性値はリセットして新たな要素を加えたところ、前のデータ表示が消えませんでした。原因は、リセットしたkey
属性値が、直前に削除した要素と重複していたためです。削除と追加を一連の処理で行うときは、削除前に重複しないkey
属性値を求めておくことで回避できました。
[追記: 2019年11月03日]「keyは振り直してはいけない」の説明を一部修正および補足。