配列のデータを元に、リストやテーブルの行を繰り返し表示するのがv-forです。基本は簡単ですが、keyの付け方やv-ifとの組み合わせで、思わぬ挙動になることがあります。この記事では、Vue 3(<script setup>)を前提に、基本から注意点までを動くコードで整理します。
配列をループして表示する
item in itemsの形で書くと、配列の要素を1つずつ取り出して、そのタグを繰り返します。:keyには、要素ごとに固有の値(ここではid)を渡します。
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, text: 'Learn Vue.js' },
{ id: 2, text: 'Build a project' },
{ id: 3, text: 'Master Vue.js' }
])
</script>
<template>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</ul>
</template>
インデックスを使う
カッコで囲んで(item, index)と書くと、現在の番号も取得できます。分割代入も使えます。inの代わりにofと書いても同じ意味です。
<template>
<!-- (要素, インデックス) の順 -->
<li v-for="(item, index) in items" :key="item.id">
{{ index }}: {{ item.text }}
</li>
<!-- 分割代入も使える。in の代わりに of でもよい -->
<li v-for="{ id, text } of items" :key="id">{{ text }}</li>
</template>
オブジェクトをループする
オブジェクトも同じv-forで繰り返せます。引数の順番は値、キー、インデックスです。
<script setup>
import { reactive } from 'vue'
const user = reactive({ name: '田中', age: 30, city: '東京' })
</script>
<template>
<!-- 値だけ表示する(keyにはキー名を使う) -->
<li v-for="(value, key) in user" :key="key">{{ value }}</li>
<!-- (値, キー, インデックス) の順 -->
<li v-for="(value, key, index) in user" :key="key">
{{ index }}. {{ key }}: {{ value }}
</li>
</template>
数を指定して繰り返す
v-forには整数も渡せます。n in 10は1から10までの値を取り出します。0からではない点に注意してください。
<template>
<!-- 1から10までの数が出る(0始まりではない) -->
<span v-for="n in 10" :key="n">{{ n }}</span>
</template>
入れ子のv-for
二次元のデータは、v-forを入れ子にして表示します。内側のkeyも、外側と同様に固有の値にします。
<script setup>
const table = [
{ id: 'r1', cells: [{ id: 'a', v: 1 }, { id: 'b', v: 2 }] },
{ id: 'r2', cells: [{ id: 'c', v: 3 }, { id: 'd', v: 4 }] }
]
</script>
<template>
<div v-for="row in table" :key="row.id">
<span v-for="cell in row.cells" :key="cell.id">{{ cell.v }}</span>
</div>
</template>
templateで複数の要素を繰り返す
1回の繰り返しで複数の要素を出したいときは、<template v-for>で囲みます。<template>自体はDOMに出力されません。このときkeyは<template>側に付けます。
<ul>
<!-- template を使うと、1回の繰り返しで複数の要素を出せる -->
<!-- key は template 側に付ける -->
<template v-for="item in items" :key="item.id">
<li>{{ item.text }}</li>
<li class="divider" role="presentation"></li>
</template>
</ul>
keyにはインデックスを使わない
keyは、Vueが各要素を識別して、画面を効率よく更新するための目印です。使える値は文字列か数値で、オブジェクトは使えません。そして、配列のインデックスをkeyにするのは避けてください。先頭への追加や並べ替えで、要素とインデックスの対応がずれ、入力欄の中身などの状態が別の行に残ることがあるためです。データごとに固有のidを使います。
<script setup>
import { ref } from 'vue'
const list = ref([
{ id: 1, text: 'A' },
{ id: 2, text: 'B' },
{ id: 3, text: 'C' }
])
function addFirst() {
list.value.unshift({ id: Date.now(), text: '新しい項目' })
}
</script>
<template>
<button @click="addFirst">先頭に追加</button>
<!-- NG:index を key にすると、並びが変わったときに入力欄の中身などがずれる -->
<div v-for="(item, index) in list" :key="index">
{{ item.text }} <input type="text">
</div>
<!-- OK:データごとに固有の id を key にする -->
<div v-for="item in list" :key="item.id">
{{ item.text }} <input type="text">
</div>
</template>
v-ifと同じ要素に書かない
Vue 3では、同じ要素にv-forとv-ifを書くとv-ifが先に評価されます。そのため、v-ifの式でv-forの変数を使うと、「todoが未定義」というエラーになります。<template v-for>で囲むか、computedで絞り込みます。
<!-- NG:同じ要素に v-for と v-if を書く
Vue 3 では v-if が先に評価されるため、todo が未定義というエラーになる -->
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo.name }}
</li>
<!-- OK①:template で囲み、中の要素に v-if を付ける -->
<template v-for="todo in todos" :key="todo.id">
<li v-if="!todo.isComplete">{{ todo.name }}</li>
</template>
<script setup>
import { ref, computed } from 'vue'
const todos = ref([
{ id: 1, name: '買い物', isComplete: false },
{ id: 2, name: '掃除', isComplete: true }
])
// OK②:computed で先に絞り込む(こちらがおすすめ)
const activeTodos = computed(() => todos.value.filter(t => !t.isComplete))
</script>
<template>
<ul>
<li v-for="todo in activeTodos" :key="todo.id">{{ todo.name }}</li>
</ul>
</template>
v-ifの基本はv-ifで条件分岐させる方法をご覧ください。
コンポーネントをv-forで繰り返す
コンポーネントにもv-forが使えます。ただし、コンポーネントのスコープは独立しているため、繰り返しのデータは自動では渡されません。propsで渡します。keyも必ず付けます。
<!-- コンポーネントはスコープが独立しているので、データは props で渡す --> <TodoItem v-for="(todo, index) in todos" :todo="todo" :index="index" :key="todo.id" />
配列を更新したときの反応
配列を変更すると、画面も自動で更新されます。push、pop、shift、unshift、splice、sort、reverseのように元の配列を変更するメソッドは、そのまま使えます。filterのように新しい配列を返すメソッドは、結果で置き換えます。
<script setup>
import { ref } from 'vue'
const items = ref([{ id: 1, text: 'A' }, { id: 2, text: 'B' }])
// 元の配列を変更するメソッド(push・pop・shift・unshift・splice・sort・reverse)は
// そのまま画面に反映される
items.value.push({ id: 3, text: 'C' })
// 元の配列を変更しないメソッド(filter など)は、結果で置き換える
items.value = items.value.filter(item => item.id !== 2)
</script>
computedの中でreverse()やsort()を直接呼ぶと、元の配列を変更してしまいます。先に[...numbers]のようにコピーしてから使ってください。
関連記事
- 検索で絞り込むリストは検索フィルター付きリストの作成
- ページに分けて表示するならページネーション付きリスト表示を実装する方法
- ドラッグで並べ替えるならリストをドラッグ&ドロップで並び替える方法
- ボタンの操作はv-onでDOMイベントを購読する方法
よくある質問(FAQ)
idを使ってください。v-ifが先に評価されるためです。v-forの変数がまだ存在せず、未定義エラーになります。computedで絞り込むか、<template v-for>で囲んでください。まとめ
v-forは、配列、オブジェクト、数のどれでも繰り返せます。keyには固有のidを使い、インデックスは避けます。v-ifとの併用はcomputedで絞り込む形にすると、エラーも防げて読みやすくなります。
