こんにちは、かつコーチです。
前回は v-model によるフォームの双方向バインディングを扱いました。
今回は、Vueの基本文法シリーズの締めくくりとして、リストを繰り返し表示する v-for と、条件によって表示・非表示を切り替える v-if を解説します。
この2つは実際の画面作りで登場しない日がないくらい頻出のディレクティブなので、ここでしっかり基礎を固めておきましょう。
v-forとは?
配列の内容を繰り返し表示する
v-for は、配列の要素の数だけ、同じテンプレートを繰り返し描画するディレクティブです。
<script setup lang="ts">
import { ref } from 'vue'
const fruits = ref<string[]>(['りんご', 'みかん', 'ぶどう'])
</script>
<template>
<ul>
<li v-for="fruit in fruits" :key="fruit">{{ fruit }}</li>
</ul>
</template>
fruits 配列の要素の数だけ <li> が生成され、それぞれに配列の中身が表示されます。
v-for="fruit in fruits" の fruit の部分は、ループの中で使う一時的な変数名で、任意の名前を付けられます。
インデックス(添字)を取得する
配列の何番目かを知りたい場合は、第2引数でインデックスを受け取れます。
<script setup lang="ts">
import { ref } from 'vue'
const fruits = ref<string[]>(['りんご', 'みかん', 'ぶどう'])
</script>
<template>
<ul>
<li v-for="(fruit, index) in fruits" :key="fruit">
{{ index + 1 }}番目:{{ fruit }}
</li>
</ul>
</template>
(fruit, index) の順番でカッコを付けて受け取るのが決まりです。
オブジェクトの配列を表示する
実務では、単純な文字列の配列よりも、オブジェクトの配列を扱う場面がほとんどです。
<script setup lang="ts">
import { ref } from 'vue'
interface User {
id: number
name: string
age: number
}
const users = ref<User[]>([
{ id: 1, name: '山田太郎', age: 28 },
{ id: 2, name: '佐藤花子', age: 34 },
])
</script>
<template>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }}({{ user.age }}歳)
</li>
</ul>
</template>
interface を使ってデータの形をあらかじめ定義しておくと、user.name のようにプロパティへアクセスするときに補完が効き、タイプミスにも気付きやすくなります。
基本の書き方:v-if
条件によって要素の表示・非表示を切り替える
v-if は、条件がtrueのときだけ要素を描画するディレクティブです。
<script setup lang="ts">
import { ref } from 'vue'
const isLoggedIn = ref<boolean>(false)
</script>
<template>
<p v-if="isLoggedIn">ようこそ、会員様</p>
<p v-else>ログインしてください</p>
</template>
v-if が false のとき、対象の要素はそもそもDOM上に生成されません(display: none で見た目だけ隠す v-show とは仕組みが異なります)。
複数の条件を分岐させたいときは、v-else-if も使えます。
<script setup lang="ts">
import { ref } from 'vue'
const score = ref<number>(75)
</script>
<template>
<p v-if="score >= 90">評価:A</p>
<p v-else-if="score >= 70">評価:B</p>
<p v-else>評価:C</p>
</template>
PHPやJavaScriptの if / elseif / else と、考え方はまったく同じです。
つまずきやすいポイント:keyの重要性
keyを付け忘れる、あるいは不適切な値を使う
v-for を使うとき、私が一番強く「これは大事だ」と実感したのが :key の役割でした。
❌ Before:keyを配列のインデックスにしてしまう
<script setup lang="ts">
import { ref } from 'vue'
interface Todo {
id: number
text: string
}
const todos = ref<Todo[]>([
{ id: 1, text: '買い物' },
{ id: 2, text: '掃除' },
{ id: 3, text: '洗濯' },
])
const removeFirst = (): void => {
todos.value.shift() // 先頭を削除する
}
</script>
<template>
<ul>
<li v-for="(todo, index) in todos" :key="index">
<input type="checkbox" />
{{ todo.text }}
</li>
</ul>
<button @click="removeFirst">先頭を削除</button>
</template>
このコードで先頭のTodoを削除すると、なぜかチェックボックスのチェック状態が別の項目に移ってしまう、という不具合を経験したことがあります。
原因は :key="index" にありました。
先頭が削除されると、2番目だった要素が「インデックス0」に繰り上がります。
Vueは key を頼りに「どの要素が変化したか」を判断しているため、インデックスをkeyにしていると「中身は変わったのに同じkeyの要素」と誤認してしまい、チェックボックスの状態のようなDOM側の情報が正しく引き継がれなくなるのです。
✅ After:一意なIDをkeyに使う
<script setup lang="ts">
import { ref } from 'vue'
interface Todo {
id: number
text: string
}
const todos = ref<Todo[]>([
{ id: 1, text: '買い物' },
{ id: 2, text: '掃除' },
{ id: 3, text: '洗濯' },
])
const removeFirst = (): void => {
todos.value.shift()
}
</script>
<template>
<ul>
<li v-for="todo in todos" :key="todo.id">
<input type="checkbox" />
{{ todo.text }}
</li>
</ul>
<button @click="removeFirst">先頭を削除</button>
</template>
:key="todo.id" のように、データが持っている一意なIDをkeyに使うことで、要素の並び替えや削除があっても、Vueは「同じidを持つ要素」を正しく追跡できるようになります。
「配列のインデックスをkeyにしない」というのは、この時の経験からいまでも徹底しているルールです。
v-forとv-ifの組み合わせに関する注意点
同じ要素に両方を書かない
v-for と v-if を同じ要素に併記したくなる場面がありますが、これは避けるべき書き方とされています。
❌ Before:同じタグにv-forとv-ifを併記する
<script setup lang="ts">
import { ref } from 'vue'
interface Todo {
id: number
text: string
done: boolean
}
const todos = ref<Todo[]>([
{ id: 1, text: '買い物', done: false },
{ id: 2, text: '掃除', done: true },
])
</script>
<template>
<!-- v-ifとv-forを同じ要素に書くのは非推奨 -->
<li v-for="todo in todos" v-if="!todo.done" :key="todo.id">
{{ todo.text }}
</li>
</template>
Vue3では v-if の方が v-for より優先度が高く評価される仕様になっており、意図と異なる挙動やパフォーマンス低下の原因になりやすいため、公式にも非推奨とされています。
✅ After:computedで絞り込んでからv-forに渡す
<script setup lang="ts">
import { ref, computed } from 'vue'
interface Todo {
id: number
text: string
done: boolean
}
const todos = ref<Todo[]>([
{ id: 1, text: '買い物', done: false },
{ id: 2, text: '掃除', done: true },
])
const incompleteTodos = computed<Todo[]>(() => {
return todos.value.filter((todo) => !todo.done)
})
</script>
<template>
<li v-for="todo in incompleteTodos" :key="todo.id">
{{ todo.text }}
</li>
</template>
第1回目の記事で扱った computed がここでも活きてきます。
「絞り込みはcomputedで済ませておき、v-for には表示したい配列だけを渡す」という役割分担を意識すると、コードの意図も読みやすくなります。
応用・一歩先の使い方
表示するものがない場合のフォールバック表示
リストが空のときに何も表示されないと、ユーザーは戸惑ってしまいます。
<script setup lang="ts">
import { ref } from 'vue'
interface Todo {
id: number
text: string
}
const todos = ref<Todo[]>([])
</script>
<template>
<ul v-if="todos.length > 0">
<li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li>
</ul>
<p v-else>タスクはまだ登録されていません</p>
</template>
v-if と v-for を別々の要素に分けて、リストが空のときは案内メッセージを出す。
小さな工夫ですが、実際のアプリのユーザー体験に直結する大事なポイントです。
まとめ
この記事のポイント
v-forは配列の要素数だけテンプレートを繰り返し描画するv-if/v-else-if/v-elseで条件に応じた表示切り替えができる:keyには配列のインデックスではなく、データが持つ一意なIDを指定するv-forとv-ifは同じ要素に併記せず、computedで絞り込んでから渡す- リストが空のときのフォールバック表示も忘れずに用意する
次に読むべき記事
ここまでで、Vue.jsの基本文法(ref/reactive、computed、v-on、v-bind、v-model、v-for/v-if)を一通り押さえました。
次回からは、いよいよComposition APIの書き方を体系的に整理していきます。
→ 次の記事:script setupの書き方:Composition APIの基本