VueJS 速習チュートリアル

Vue v-for コンポーネント

コンポーネント(Components)v-for と組み合わせることで、同じ種類の要素を大量に生成し、再利用することができます。

v-for を用いてコンポーネントを生成する際、配列(Array) の値に基づいて プロップス(Props) を動的に割り当てられる点は、開発において非常に強力なメリットとなります。

1. v-for によるコンポーネントの生成

ここでは、食べ物の名前が含まれた配列に基づき、v-for を使ってコンポーネントを生成してみましょう。

1.1 実装例:基本的なコンポーネントループ

App.vue:

<template>
  <h1>食べ物</h1>
  <p>配列に基づいて v-for で作成されたコンポーネントです。</p>
  <div id="wrapper">
    <food-item
      v-for="x in foods"
      v-bind:food-name="x"/>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        foods: ['リンゴ', 'ピザ', 'ライス', '魚', 'ケーキ']
      };
    }
  }
</script>

FoodItem.vue:

<template>
  <div>
    <h2>{{ foodName }}</h2>
  </div>
</template>

<script>
  export default {
    props: ['foodName']
  }
</script>

2. v-bind の省略記法

プロップスを動的にバインドするには v-bind を使用しますが、今後 v-bind を多用することになるため、本チュートリアルの以降のセクションでは省略記法である : を使用します。

3. key 属性の役割と重要性

v-for で要素を作成した後に配列を操作(削除や並び替えなど)すると、Vue が要素を更新する仕組みが原因でエラーや予期せぬ挙動が発生することがあります。

Vue はパフォーマンスを 最適化(Optimization) するために、要素を再利用します。アイテムが削除された際、すべての要素を再作成するのではなく、既存の要素を再利用しようとしますが、その際に要素のプロパティが正しく保持されない可能性があります。

要素が正しく再利用されない原因は、各要素に ユニークな識別子(Unique Identifier) がないためです。そこで、Vue が各要素を明確に区別できるようにするために使用するのが key 属性 です。

4. 挙動の不具合:key 属性がない場合

key 属性がない場合に発生する不適切な挙動を確認するために、食べ物の名前、説明、お気に入り画像、およびお気に入り状態を切り替えるボタンを表示するページを作成してみましょう。

App.vue:

<template>
  <h1>食べ物</h1>
  <p>food 配列から v-for を使ってアイテムを生成しています。</p>
  <div id="wrapper">
    <food-item
      v-for="x in foods"
      :food-name="x.name"
      :food-desc="x.desc"
      :is-favorite="x.favorite"/>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        foods: [
          { name: 'リンゴ', desc: 'リンゴは木に実る果物の一種です。', favorite: true },
          { name: 'ピザ', desc: 'ピザはパン生地、トマトソース、チーズなどで構成されます。', favorite: false },
          { name: 'ライス', desc: 'ライスは多くの人が好んで食べる穀物です。', favorite: false },
          { name: '魚', desc: '魚は水中に生息する動物です。', favorite: true },
          { name: 'ケーキ', desc: 'ケーキは味が良い甘い食べ物です。', favorite: false }
        ]
      };
    }
  }
</script>

<style>
  #wrapper {
    display: flex;
    flex-wrap: wrap;
  }
  #wrapper > div {
    border: dashed black 1px;
    flex-basis: 120px;
    margin: 10px;
    padding: 10px;
    background-color: lightgreen;
  }
</style>

FoodItem.vue:

<template>
  <div>
    <h2>
      {{ foodName }}
      <img src="/img_quality.svg" v-show="foodIsFavorite">
    </h2>
    <p>{{ foodDesc }}</p>
    <button v-on:click="toggleFavorite">お気に入り</button>
  </div>
</template>

<script>
  export default {
    props: ['foodName','foodDesc','isFavorite'],
    data() {
      return {
        foodIsFavorite: this.isFavorite
      }
    },
    methods: {
      toggleFavorite() {
        this.foodIsFavorite = !this.foodIsFavorite;
      }
    }
  }
</script>

<style>
  img {
    height: 1.5em;
    float: right;
  }
</style>

ここで、配列の2番目の要素を削除するボタンを作成します。key 属性がない状態でこれを実行すると、「魚」のお気に入り画像が「ケーキ」の要素に移ってしまうといった、正しくない挙動 が発生します。

4.1 不具合が発生する追加コード (App.vue)

<button @click="removeItem">アイテムを削除</button>
methods: {
  removeItem() {
    // 2番目のアイテムを削除
    this.foods.splice(1, 1);
  }
}

この「アイテム削除時にお気に入り画像がズレる」問題は、Vue が要素を再利用してページを最適化しようとする一方で、各要素を完全に見分けることができていないために起こります。

5. 解決策:一意の key 属性を指定する

v-for で要素を生成する際は、各要素を一意にマークするために常に key 属性 を含めるべきです。key 属性を使用すれば、この問題は解消されます。

なお、配列の インデックス(Index)key 属性の値として使用するのは避けてください。配列の要素が追加・削除されるたびにインデックスが変わってしまうためです。ID番号などのユニークなデータプロパティを作成するのが理想的ですが、今回の食べ物リストのように名前が重複しない場合は、その名前をそのまま key として利用できます。

5.1 修正後の実装 (App.vue)

App.vue に以下の1行を追加して各要素を一意に識別させることで、問題を解決できます。

<food-item
  v-for="x in foods"
  :key="x.name"
  :food-name="x.name"
  :food-desc="x.desc"
  :is-favorite="x.favorite"/>