Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

v-forディレクティブを使うと、データに納められた複数の項目がページに要素として差し込めます。このときv-forkeyを与えるのが、「スタイルガイド」に定められた「必須」のルールです(「キー付き v-for」)。

一意のkey特別属性とインデックス

key特別属性は、要素に一意の値を与えます。:keyv-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■どの項目を削除しても最後の項目がアニメーションする

1807001_001.png
最初の項目を削除

1807001_002.png
アニメーションするのは最後の項目

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は振り直してはいけない」の説明を一部修正および補足。

FumioNonaka
はなし家、もの書き、コード書き。詳しくはwebで。 担当講座: 電気通信大学ウェブシステムデザインプログラム「Web UI・UXプログラミング演習I・Ⅱ」 https://www.websys.edu.uec.ac.jp/ ロクナナワークショップ「Vue.js入門講座」 https://67.org/ws/workshop/detail/0136javascript.html
http://www.fumiononaka.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away