Vue.js: v-forで項目インデックスをkey属性にしていいのか



[`v-for`ディレクティブ](https://jp.vuejs.org/v2/api/#v-for)を使うと、データに納められた複数の項目がページに要素として差し込めます。このとき`v-for`に`key`を与えるのが、「[スタイルガイド](https://jp.vuejs.org/v2/style-guide/)」に定められた「必須」のルールです(「[キー付き v-for](https://jp.vuejs.org/v2/style-guide/#キー付き-v-for-必須)」)。


# 一意のkey特別属性とインデックス
[`key`特別属性](https://jp.vuejs.org/v2/api/#key)は、要素に一意の値を与えます。`:key`は`v-bind:key`の省略構文です(「[key](https://jp.vuejs.org/v2/guide/list.html#key)」参照)。

```html
<ul>
	<li v-for="item in items" :key="item.id">...</li>
</ul>
```

また、配列から`v-for`でデータを取り出すとき、第2引数に配列インデックスが受け取れます(「[v-for で配列に要素をマッピングする](https://jp.vuejs.org/v2/guide/list.html#v-for-で配列に要素をマッピングする)」参照)。インデックスはもちろん一意です。


```html
<ul id="example">
	<li v-for="(item, index) in items">
		{{parentMessage}} - {{index}} - {{item.message}}
	</li>
</ul>
```

では、`v-for`で配列インデックスを得て、`key`に与えればよいのではないでしょうか。結論からいいますと、配列インデックスは`key`の値とすべきではありません。

> `key`を与えない場合は、[配列インデックスが使われる](https://github.com/vuejs/vue/issues/5410#issuecomment-292923871)とのことです。

> また、[要素の追加や削除あるいは順序変更がない場合](https://github.com/vuejs/vue/issues/6235#issuecomment-402720536)にかぎれば、`key`を使わなくても問題はないようです。むしろ「[標準のモードは効率がいい](https://jp.vuejs.org/v2/guide/list.html#key)」とされます。ただ、あとで変わりやすい前提と思われるので、`key`を「必須」にしているのではないかと推測されます。


# インデックスをキーにすると要素のアニメーションが意図しない動きになる
`v-for`で得た配列インデックスを`key`の値に与えると、要素のアニメーションが意図しない動きになったりします。「[Vue.js: 動的に変更する要素にアニメーションを加える](http://www.fumiononaka.com/Business/html5/FN1711001.html)」でつくったTodoリストは、項目を加えたり除いたりすると水平アニメーションします(サンプル001)。

#### サンプル001■Vue.js + ES6: TodoMVC with animation
<p class="codepen" data-height="360" data-theme-id="0" data-default-tab="js,result" data-user="FumioNonaka" data-slug-hash="YMzjve" style="height: 360px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;" data-pen-title="Vue.js + ES6: TodoMVC with animation">
  <span>See the Pen <a href="https://codepen.io/FumioNonaka/pen/YMzjve/">
  Vue.js + ES6: TodoMVC with animation</a> by Fumio Nonaka (<a href="https://codepen.io/FumioNonaka">@FumioNonaka</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

```html
<li v-for="todo in filteredTodos"

	:key="todo.id"
	>

</li>
```

データをはじめに読み込んだとき(todoStorage.fetch())は、配列インデックスを項目の`key`に与え、配列の長さを新たに加える項目の値(todoStorage.uid)としています。そのうえで、項目を加える(addTodo())たびに値を加算するのです。`key`の値は一意であるとともに、ひとたびつけられた項目の値は決して変わりません。

```javascript
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)。

```html
<li v-for="(todo, index) in filteredTodos"

	:key="index"
	>

</li>
```

#### サンプル002■Vue.js + ES6: Setting the index of item to its :key
<p class="codepen" data-height="360" data-theme-id="0" data-default-tab="js,result" data-user="FumioNonaka" data-slug-hash="MRWBZa" style="height: 360px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;" data-pen-title="Vue.js + ES6: Setting the index of item to its :key">
  <span>See the Pen <a href="https://codepen.io/FumioNonaka/pen/MRWBZa/">
  Vue.js + ES6: Setting the index of item to its :key</a> by Fumio Nonaka (<a href="https://codepen.io/FumioNonaka">@FumioNonaka</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

すると、なんということでしょう。どの項目を削除してもアニメーションするのは最後の項目になってしまいます。

#### 図001■どの項目を削除しても最後の項目がアニメーションする
![1807001_001.png](https://qiita-image-store.s3.amazonaws.com/0/128949/c51cbc6d-06fc-e6cb-e441-2c71146656a3.png)
最初の項目を削除

![1807001_002.png](https://qiita-image-store.s3.amazonaws.com/0/128949/2027e293-041a-d347-277f-11f35f623a21.png)
アニメーションするのは最後の項目


# keyは振り直してはいけない
一意の`key`は、どのデータが変わったのか、Vueが追いかけるために用いられます。ところが、配列インデックスを使ってしまうと、前の要素が除かれたら後の番号はつけ変えなければなりません。すると、Vueはどの要素のデータが変わったのか後追いできなくなります。つまり、`key`は振り直してはいけないのです。

それでも、データの削除そのものは正しく行われます。けれども、アニメーションが求められると、要素をすぐに消すわけにはいきません。配列インデックスが使われた場合、Vueはデータとの対応は見ず、最後の要素を削除するようです(「[キー付き v-for](https://jp.vuejs.org/v2/style-guide/#キー付き-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)](https://github.com/vuejs/vuejs.org/issues/1161#issuecomment-333353839)」より)

> たとえば、配列の最初の項目が除かれたとしましょう。もしインデックスを使っていたら、Vueは最後の要素が削除されたとみなして、残った配列データで要素を順に更新し、すべてが変更されることになります。(筆者訳)

前掲サンプル001は、もとデータの配列の長さをつぎの`key`の値とし、項目が加わるたびに加算しました。一意であるとともに、変わらぬ値が振られるので確実です。もっとも、`key`の値を画面に表示したとき、追加・削除を繰り返すと、番号の開きが大きくなるのは気になるかもしれません。その場合は「[追加と削除が繰り返される配列要素のオブジェクトに一意のid番号を振る](https://qiita.com/FumioNonaka/items/63ce689b3599a6922182)」をお読みください。

[追記: 2019年09月03日] 関連でつまずいた問題を、覚書として加えます。データの配列が空(`length`が0)になったとき、続けて`key`属性値はリセットして新たな要素を加えたところ、前のデータ表示が消えませんでした。原因は、リセットした`key`属性値が、直前に削除した要素と重複していたためです。削除と追加を一連の処理で行うときは、削除前に重複しない`key`属性値を求めておくことで回避できました。

[追記: 2019年11月03日]「keyは振り直してはいけない」の説明を一部修正および補足。