【徹底解説】これを見ればわかるvue.jsのv-forのkeyの動作

vue.jsでv-forディレクティブを使う際にv-bind:keyを設定するとどのような効果や変化があるのかわからない人も多いかと思います。特にVue.jsを使い始めた人であればv-bind:keyを設定しなければエディターに警告が表示されるからと意味もわからずに設定する人も多いのではないでしょうか。マニュアルを読んでもわからない時におすすめなのがChomeのデベロッパーツールを利用することです。デベロッパーツールを確認すると視覚的に要素の動きがわかるのでv-bind:keyを理解したい人はぜひ参考にしてみてください。
Vue2, Vue3で動作確認を行なっています。
目次
テーブルの削除に利用
v-bind:keyを使用しない場合
v-forディレクティブを使って作成したテーブルの行削除を通してv-bind:keyの動作確認を行います。v-bindという記法ではなくv-bindを省略して:keyと記述することもできます。
最初はv-bind:keyの設定を行わずに行の削除を行うためtr要素にv-bindの設定は入っていません。
<div id="app">
<table class="table table-bordered">
<thead class="thead-dark">
<tr>
<th>ID</th>
<th>ユーザ名</th>
<th>削除</th>
</tr>
</thead>
<tbody>
<tr v-for="(user,index) in users">
<td>@{{ user.id }}</td>
<td>@{{ user.email }}</td>
<td><button class="btn btn-danger" v-on:click="deleteRow(index)">削除</td>
</tr>
</tbody>
</table>
</div>
Vueインスタンスの中にメソッドdeleteRowを追加します。削除ボタンを押した行のindexを受け取り、そのindex行の削除を行っているだけのシンプルなコードです。データは独自に作成したバックエンドサーバから取得しています。このままのコードを利用しても動作確認を行うことはできません。実際に手元で動作確認をしたい場合は”Vue3でindex.htmlでコードを作成”を参考にしてください。

var app = new Vue({
el: '#app',
data: {
users: []
},
methods: {
deleteRow : function(index){
this.$delete(this.users,index)
}
},
mounted: function(){
axios.get('/api/users').then(response => this.users = response.data);
}
})

Chromeのデベロッパーツールを開いてElementsタブで下記のようにtdの要素が見えるように展開してください。

先頭の行の削除ボタンを押すと下記に色が付いている部分で更新が行われるのがわかります。行の削除を行うとブラウザ上では行が消えますが、実際には先頭の要素が削除されるのではなく下の行情報によって上書きされていることが確認できます。

Vue3でindex.htmlでコードを作成
index.htmlファイルをローカルに作成して動作確認を行いたい場合は下記のコードを利用してください。Vue3のOptions APIで設定を行なっています。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<title></title>
</head>
<body>
<div id="app">
<table class="table table-bordered">
<thead class="thead-dark">
<tr>
<th>ID</th>
<th>ユーザ名</th>
<th>削除</th>
</tr>
</thead>
<tbody>
<tr v-for="(user,index) in users">
<td>{{ user.id }}</td>
<td>{{ user.email }}</td>
<td><button class="btn btn-danger" v-on:click="deleteRow(index)">削除</td>
</tr>
</tbody>
</table>
</div>
<script>
const { createApp } = Vue
createApp({
data() {
return {
users:[]
}
},
methods:{
deleteRow(index){
this.users.splice(index,1)
}
},
mounted(){
axios.get('https://jsonplaceholder.typicode.com/users').then(response => this.users = response.data);
}
}).mount('#app')
</script>
</body>
</html>
スタイルの設定を行なっていないのでブラウザで確認すると以下のような表示となります。

v-bind:keyを使用する場合
今度は、v-forディレクティブにv-bind:keyをつけて削除をおこなってみましょう。
<tr v-for="(user,index) in users v-bind:key="user.id">
削除ボタンを押すとブラウザ上では押した行の削除が行われます。デベロッパーツールを見るとv-bindを設定した場合は先程のようにtd要素に色がつくことはなくボタンを押したtr要素のみが削除されます。各行でデータの上書きは行われていないことがわかります。

ここまで動作を簡単なイラストで書くとv-bind:keyがない場合は、各行の器となるtr要素は残ったまま、その器に入っているtdの中身だけ移動しており、v-bind:keyがある場合は、tr要素の器そのものが消えます。

ここまでの比較でv-bindの有無により、要素の削除と要素の上書きの差があることがわかりました。しかし、デベロッパーツールで確認しない限りはブラウザ上では両者の差はわかりません。では、なぜv-bind:keyをつける必要があるのでしょう。inputの要素を追加することでv-bind:keyをつけなかった場合の問題がはっきりとわかります。
input要素を使用
v-bindを利用しない場合
先ほど使用していたテーブルに新たにinput要素の列の追加を行います。
<table class="table table-bordered">
<thead class="thead-dark">
<tr>
<th>ID</th>
<th>ユーザ名</th>
<th>入力</th>
<th>削除</th>
</tr>
</thead>
<tbody>
<tr v-for="(user,index) in users">
<td>@{{ user.id }}</td>
<td>@{{ user.name }}</td>
<td><input type="text" class="form-control"></td>
<td><button class="btn btn-danger" v-on:click="deleteRow(index)">削除</td>
</tr>
</tbody>
</table>

追加したinput要素に文字列を入力します。

入力した状態のままinput要素に入力を行ったID=3の行の削除ボタンをクリックします。inputのセルも行と一緒に削除されるものだろうと予想したと思うのですが、inputの要素だけ残ったままの状態になります。

デベロッパーツールの力を借りて、どのような変化が起きているか確認します。削除ボタンを押すとinput要素以外のtd要素には色が付いており更新が行われるのがわかりますがinput要素はそのままの状態で変化がないということがわかります。v-bind:keyをなしの状態でinput要素を使うと不具合の原因になることがわかりました。

v-bindを利用する場合
最後にv-bind:keyを設定するとinput要素に入力した文字列がどうなる確認します。
<tr v-for="(user,index) in users" v-bind:key="user.id">

削除ボタンを押すとブラウザ上からもinput要素も一緒に削除されることが確認できます。

デベロッパーツールを見てもtrの要素が削除されるのが確認できます。
v-bind:keyをつけることでinput要素の不具合が解消されることが確認できました。
v-bind:key=”index”の場合は
v-bind:key=”index”を設定して実施するとkeyの設定しない場合と結果は同じになります。v-bind:keyではindexの利用ではなく、idなどその要素を一意に識別できる要素を設定する必要があることがわかります。
<tr v-for="(user,index) in users" v-bind:key="index">
以下はマニュアルからのv-bind:keyの設定の抜粋になりますが、もう一度読み直して今回の文書が少しでも理解の助けになればと思います。正直なところ動作確認をせず下を読んでも意味がさっぱり分かりませんでした。
