Vue 3 の条件付きレンダリングとリストレンダリング:v-if と v-for の詳細な比較
条件付きレンダリング(v-if / v-show)とリストレンダリング(v-for)は、Vueテンプレートで最もよく使われる2つのディレクティブです。前者は要素を表示するかどうかを制御し、後者は表示される要素の数を制御します。これらを組み合わせることで、UIのシナリオの90%に対応できます。
v-ifとv-forの5つの主な違いを理解し、:keyの正しい使い方を把握することは、高性能なVueアプリケーションを構築するために不可欠です。
1. 学習内容
条件付きレンダリング
v-showとv-ifの5つの主な違いv-for配列、オブジェクト、数値、文字列の反復処理:keyの目的とベストプラクティスtemplate複数の要素を囲む- リストのフィルタリング、並べ替え、ページネーション
- 仮想スクロールの最適化(10,000件以上の項目)
2. コードの悪夢:ダッシュボードにおける「ネストされたif文」
(1) 課題:5つ入れ子になった v-if タグ――デバッグの悪夢
アリスのダッシュボードのコードが手に負えなくなってしまった:
VUE
<!-- ❌ The "Flip" Version:v-if Nested Hell -->
<template>
<div v-if="user">
<div v-if="user.isAdmin">
<div v-if="orders.length > 0">
<div v-if="!isLoading">
<div v-if="!hasError">
<!-- Content is finally displayed -->
<p>Orders: {{ orders.length }}</p>
</div>
<div v-else>Error: {{ errorMsg }}</div>
</div>
<div v-else>Loading...</div>
</div>
<div v-else>No orders</div>
</div>
<div v-else>Not admin</div>
</div>
<div v-else>Please login</div>
</template>
チャーリーによるコードレビュー:
「アリス、これじゃ読めないよ。v-ifが5段階もネストされてる。もし条件をあと2つ追加したらどうなる?もっとすっきりしたパターンが必要だ。」
(2) Vue での解決策:v-if によるフラット化 + 計算プロパティ
VUE
<template>
<div v-if="!user">Please login</div>
<div v-else-if="!user.isAdmin">Not admin</div>
<div v-else-if="isLoading">Loading...</div>
<div v-else-if="hasError">Error: {{ errorMsg }}</div>
<div v-else-if="orders.length === 0">No orders</div>
<div v-else>
<p>Orders: {{ orders.length }}</p>
<OrderList :orders="orders" />
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps(['user', 'orders', 'isLoading', 'hasError', 'errorMsg'])
// Flatten 5 conditions (v-else-if chain)
</script>
(3) 収益
- 可読性:5段階のネスト → 6行が横並びに配置
- メンテナンス性:条件を追加するには、
v-else-ifを1行追加するだけで済みます。 - デバッグ:各行に独自の条件が設定されているため、ブレークポイントを簡単に設定できます
3. v-if / v-else-if / v-else による条件付きレンダリング
(1) 基本的な構文
VUE
<template>
<!-- Single condition -->
<p v-if="isVisible">Visible</p>
<!-- Choose one of the two -->
<p v-if="score >= 60">Passed</p>
<p v-else>Failed</p>
<!-- Choose one or more -->
<p v-if="type === 'A'">Type A</p>
<p v-else-if="type === 'B'">Type B</p>
<p v-else-if="type === 'C'">Type C</p>
<p v-else>Unknown</p>
</template>
(2) template要素
VUE
<template>
<!-- template:Grouping Multiple Elements,Do not render DOM -->
<template v-if="isAdmin">
<h1>Admin Dashboard</h1>
<p>Welcome, {{ user.name }}</p>
<button>Edit</button>
</template>
<!-- Does not render template element (Savings 1 DOM layer) -->
</template>
(3) 5つの主なユースケース
| シナリオ | v-if の使用 |
|---|---|
| ログイン中/未ログイン | ✅ |
| 読み込み中.../読み込み完了 | ✅ |
| データなし/データあり | ✅ |
| エラー状態 | ✅ |
| 表示/非表示を頻繁に切り替える | ❌ (v-show を使用) |
4. v-show と v-if の 5 つの主な違い
(1) 主な相違点
VUE
<template>
<!-- v-if: When conditions are false, Elements removed from DOM -->
<p v-if="show">v-if</p>
<!-- show=false: The element does not exist in DOM -->
<!-- v-show: When conditions are false, display: none -->
<p v-show="show">v-show</p>
<!-- show=false: <p style="display: none">v-show</p> -->
</template>
(2) 5つの主な比較点
| 属性 | v-if | v-show |
|---|---|---|
| DOMの操作 | 条件がfalseのときは要素を削除 | 常にDOMに残り、displayプロパティのみが切り替わる |
| スイッチング性能 | 低い(その都度破棄・再構築) | 高い(CSSのみ) |
| 初期レンダリング | 条件がfalseの場合はレンダリングしない | 常にレンダリングする(非表示の場合でも) |
| トランジションとの互換性 | ✅ 完全対応(イン/アウトアニメーション) | ❌ 未対応 |
| 利用シーン | たまに切り替える場合 | 頻繁に切り替える場合 |
(3) 選定に関する推奨事項
JS
// Switch Occasionally(If you are logged in/Not logged in)→ v-if
<div v-if="isLoggedIn">Welcome, {{ user.name }}</div>
<div v-else>Please login</div>
// Frequent switching (e.g. tab switch) → v-show
<button v-show="isActive">Active</button>
<button v-show="!isActive">Inactive</button>
5. v-for を使ったリストのレンダリング
(1) 配列の反復処理
VUE
<template>
<!-- Basic Iteration -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<!-- Indexed -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index + 1 }}. {{ item.name }}
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' }
])
</script>
(2) オブジェクトの反復処理
VUE
<template>
<ul>
<li v-for="(value, key, index) in user" :key="key">
{{ index + 1 }}. {{ key }}: {{ value }}
</li>
</ul>
</template>
<script setup>
import { reactive } from 'vue'
const user = reactive({
name: 'Alice',
age: 25,
email: 'alice@example.com'
})
// Output:1. name: Alice 2. age: 25 3. email: alice@example.com
</script>
(3) 数値の反復処理
VUE
<template>
<!-- Rendering 1 to 10 -->
<span v-for="n in 10" :key="n">{{ n }}</span>
<!-- Output:12345678910 -->
</template>
(4) 文字列の反復処理
VUE
<template>
<!-- One character per line span -->
<span v-for="char in 'Hello'" :key="char">{{ char }}</span>
<!-- Output:H e l l o -->
</template>
6. :key の目的とベストプラクティス
(1) :key とは何ですか?
:key VueがDOMを効率的に更新できるよう、各v-for項目に一意の識別子を割り当てます。
VUE
<template>
<!-- ❌ None :key(Vue Use by default index) -->
<li v-for="item in items">{{ item.name }}</li>
<!-- ✅ Has :key (Use "unique" id) -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</template>
(2) なぜ :key が必要なのでしょうか?
graph LR A[Original List A B C] --> B[New List B C D] B --> C[None key:Possible misalignment] B --> D[Has key: Update Correctly] style C fill:#ff6b6b style D fill:#42b883
:key: が存在しない場合
- Vueでの
indexの使用比較 - Aを削除すると、B/Cは(0)/(1)になりますが、Vueは「B/Cはまだ存在している」と認識してしまいます
- コンポーネントの状態の不整合を引き起こす
:key が存在する場合:
- VueでのIDの比較
- どの項目が追加、削除、または移動されたかを正確に特定する
- 最適なパフォーマンス
(3) :key のベストプラクティス
VUE
<!-- ✅ Recommendations:The Only One ID -->
<li v-for="user in users" :key="user.id">
<!-- ✅ Second choice:Unique Field(Email、Mobile phone number, etc.)-->
<li v-for="user in users" :key="user.email">
<!-- ❌ Avoid:index(When the data changes, the alignment is off.)-->
<li v-for="(user, index) in users" :key="index">
<!-- ❌ Never, under any circumstances:random()(Every render is different,Forced Rebuild)-->
<li v-for="user in users" :key="Math.random()">
7. リストのフィルタリング/並べ替え/ページネーション
(1) フィルタリングと並べ替え(計算プロパティ)
JS
import { ref, computed } from 'vue'
const items = ref([
{ id: 1, name: 'iPhone', price: 999, category: 'phone' },
{ id: 2, name: 'MacBook', price: 2499, category: 'laptop' },
{ id: 3, name: 'iPad', price: 599, category: 'tablet' }
])
const searchQuery = ref('')
const selectedCategory = ref('all')
const sortBy = ref('price') // 'price' / 'name'
// Filter + Sort(computed Automatic Caching)
const filteredItems = computed(() => {
let result = items.value
// 1. Filter by search term
if (searchQuery.value) {
result = result.filter(item =>
item.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
}
// 2. Filter by Category
if (selectedCategory.value !== 'all') {
result = result.filter(item => item.category === selectedCategory.value)
}
// 3. Sort
result = [...result].sort((a, b) => {
if (sortBy.value === 'price') return a.price - b.price
if (sortBy.value === 'name') return a.name.localeCompare(b.name)
return 0
})
return result
})
(2) ページネーション
JS
const currentPage = ref(1)
const pageSize = ref(10)
// Data after pagination
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return filteredItems.value.slice(start, start + pageSize.value)
})
// Total Number of Pages
const totalPages = computed(() =>
Math.ceil(filteredItems.value.length / pageSize.value)
)
8. 完全な例:動的な商品リスト
(1) ▶ サンプル:. v-if/v-show の5つの活用法
VUE
<template>
<!-- 1. v-if:Conditional Rendering(Not here DOM) -->
<p v-if="show">v-if</p>
<!-- 2. v-show:display none(Always there DOM) -->
<p v-show="show">v-show</p>
<!-- 3. v-else-if:Chained Conditions -->
<p v-if="type === 'A'">A</p>
<p v-else-if="type === 'B'">B</p>
<p v-else>C</p>
<!-- 4. v-if + template:Multi-element -->
<template v-if="isAdmin">
<h1>Admin</h1>
<button>Edit</button>
</template>
<!-- 5. v-if vs v-show Performance Comparison -->
<div v-if="occasionally">Switch Occasionally</div>
<div v-show="frequently">Frequent switching(Not recommended for use in combination with `<transition>`,v-if That's the only way)</div>
</template>
(2) ▶ サンプル:. v-forの5つの活用法
VUE
<template>
<!-- 1. Array -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
<!-- 2. Array + Index -->
<li v-for="(item, i) in items" :key="item.id">
{{ i + 1 }}. {{ item.name }}
</li>
<!-- 3. Object -->
<li v-for="(value, key) in user" :key="key">
{{ key }}: {{ value }}
</li>
<!-- 4. Numbers -->
<span v-for="n in 5" :key="n">{{ n }}</span>
<!-- 5. String -->
<span v-for="char in 'Hello'" :key="char">{{ char }}</span>
</template>
(3) ▶ サンプル:. :key の 5 つの使い方の比較
VUE
<template>
<!-- 1. The Only One ID(Best) -->
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
<!-- 2. Unique Field -->
<li v-for="user in users" :key="user.email">{{ user.name }}</li>
<!-- 3. Composite key -->
<li v-for="post in posts" :key="`${post.userId}-${post.id}`">
{{ post.title }}
</li>
<!-- 4. Use index (Second choice) -->
<li v-for="(item, i) in items" :key="i">{{ item.name }}</li>
<!-- 5. ❌ random()(Never, under any circumstances) -->
<li v-for="item in items" :key="Math.random()">{{ item.name }}</li>
</template>
(4) ▶ サンプル:. リストのフィルタリング+ソート+ページネーションの完全な例
VUE
<template>
<div>
<!-- Search box -->
<input v-model="searchQuery" placeholder="Search...">
<!-- Filter by Category -->
<select v-model="selectedCategory">
<option value="all">All</option>
<option value="phone">Phone</option>
<option value="laptop">Laptop</option>
</select>
<!-- Sort -->
<select v-model="sortBy">
<option value="price">Price</option>
<option value="name">Name</option>
</select>
<!-- List -->
<ul>
<li v-for="item in paginatedItems" :key="item.id">
{{ item.name }} - ${{ item.price }}
</li>
</ul>
<!-- Pagination -->
<button :disabled="currentPage === 1" @click="currentPage--">
Previous
</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
<button :disabled="currentPage === totalPages" @click="currentPage++">
Next
</button>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const items = ref([
{ id: 1, name: 'iPhone', price: 999, category: 'phone' },
{ id: 2, name: 'MacBook', price: 2499, category: 'laptop' },
{ id: 3, name: 'iPad', price: 599, category: 'tablet' }
])
const searchQuery = ref('')
const selectedCategory = ref('all')
const sortBy = ref('price')
const currentPage = ref(1)
const pageSize = 10
const filteredItems = computed(() => {
let result = items.value
if (searchQuery.value) {
result = result.filter(i => i.name.includes(searchQuery.value))
}
if (selectedCategory.value !== 'all') {
result = result.filter(i => i.category === selectedCategory.value)
}
return [...result].sort((a, b) => {
if (sortBy.value === 'price') return a.price - b.price
return a.name.localeCompare(b.name)
})
})
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * pageSize
return filteredItems.value.slice(start, start + pageSize)
})
const totalPages = computed(() =>
Math.ceil(filteredItems.value.length / pageSize)
)
</script>
(5) ▶ サンプル:. よくある5つの間違い
| エラー | 症状 | 解決策 |
|---|---|---|
| なし :key | リストの位置ずれ、パフォーマンスの低下 | :key="item.id" を追加 |
| v-for と v-if の併用 | v-if の優先順位の混乱 | template で囲むか、computed を使用する |
| キーとして「index」を使用する | ソート時の不整合 | 一意のIDを使用する |
v-show は <transition> をサポートしていません |
アニメーション化できません | 代わりに v-if を使用してください + <transition> |
| リストが大きすぎる(100,000件以上) | ページがフリーズする | 仮想スクロールには vue-virtual-scroller を使用する |
(6) ▶ サンプル:. v-if と v-for の優先順位
VUE
<!-- In Vue 3, v-if has higher priority than v-for (Vue 2 is the opposite) -->
<ul>
<!-- Vue 3: First determine v-if, then v-for -->
<li v-for="user in users" v-if="user.isActive" :key="user.id">
{{ user.name }}
</li>
<!-- ✅ But Vue officially recommends using template to separate -->
<!-- ❌ Incorrect wording(Not recommended) -->
<li v-for="user in users" v-if="shouldShowUsers(user)" :key="user.id">
{{ user.name }}
</li>
<!-- ✅ Recommendations:computed Filter -->
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
</template>
❓ よくある質問
Q v-if と v-show のどちらを選べばいいですか?
A たまに切り替える場合は v-if を使い(初期レンダリングの負荷を軽減するため)、頻繁に切り替える場合は v-show を使います(DOM の破壊と再構築を繰り返さないため)。ログイン中/ログアウト中の状態の切り替えには v-if を、タブの切り替えには v-show を使用してください。
Q
:key は v-for と併用する必要がありますか?A パフォーマンスの問題やコンポーネントの状態の不一致を避けるため、
:key の使用を強く推奨します。一意の ID (item.id) を最優先とし、次いで index を使用してください。random() は絶対に使用しないでください。Q v-if と v-for を併用する場合、優先順位はどうなりますか?
A Vue 3 では、v-if が v-for より優先されます(Vue 2 とは逆です)。ただし、公式の推奨としては、これらを併用することは避け、代わりに
template を使用して分離するか、computed を使用してフィルタリングすることを推奨しています。Q
template要素の目的は何ですか?A
templateは、DOMにはレンダリングされないVueのロジックラッパー要素です。これは、v-ifやv-forが複数の要素を含む場合に、不要なdiv要素の生成を防ぐために使用されます。Q リストのパフォーマンスが低い場合はどうすればよいですか?
A 3つの最適化方法があります:(1)
:keyを追加する;(2) computedを使用してフィルタリング/ソート結果をキャッシュする; (3) リストが大きい場合(10万件以上)、vue-virtual-scroller を使用して仮想スクロールを行い、表示領域のみを描画するようにする。Q
v-for を使ってオブジェクトを反復処理する際、キーと値にアクセスできますか?A はい。
v-for="(value, key, index) in obj" を使用します。パラメータの順序は、値、キー、インデックスです。Q データがない場合にプロンプトを表示するにはどうすればよいですか?
A v-if と v-else を組み合わせて使用します:
<div v-if="items.length">Column </div><div v-else> Data</div>。Q リスト内の選択した項目を強調表示するにはどうすればよいですか?
A 各項目に
activeIndex を追加してください:@click="activeIndex = item.id" :class="{ active: activeIndex === item.id }"。📖 まとめ
- v-if は条件付きレンダリング(DOM から削除されます)、v-show は
displayプロパティのオン/オフを切り替えます(常に DOM に残ります) - たまに交換する場合は
v-ifを、頻繁に交換する場合はv-showを使用してください v-forを使用して、配列、オブジェクト、数値、文字列を反復処理します。:keyを使用すると、パフォーマンスが向上します。- :key: 一意のIDが最適です。次善の策としてはインデックスが挙げられます。random()は使用しないでください。
template要素は複数の要素を囲んでいますが、DOMにはレンダリングされませんcomputedを使用して、リストのフィルタリング、並べ替え、ページネーションを自動的にキャッシュする- Vue 3 では、
v-ifがv-forより優先されます - 仮想スクロールによる大規模リスト(10万件以上)の最適化
📝 練習問題
(1) 基礎問題(難易度:⭐)
v-if/v-else を使用して、ログイン状態とログアウト状態を切り替えます:
- ログイン中:ユーザー名と「ログアウト」ボタンを表示します
- ログインしていない場合:「ログインしてください」というメッセージとログインフォームが表示されます
- テンプレートを使用して、複数の要素をグループ化する
(2) 上級問題(難易度:⭐⭐)
v-for を使用した To-Do リストの実装:
- 3つのステータス:「すべて」「進行中」「完了」(3つのボタンを使って切り替える)
- このリストには、各タスクのステータスとともにタスクが表示されます
- 各項目には「完了」ボタンと「削除」ボタンがあります
- 下部に「未完了:N件」と表示されます
(3) チャレンジ問題(難易度:⭐⭐⭐)
製品のフィルタリング、ソート、ページネーションのシステムを完全に実装する(5ファイル):
- 商品データ:30件。商品名、価格、カテゴリ、作成日時が含まれる。
- 検索:名前によるあいまい検索を実行する
- カテゴリで絞り込み:5つのカテゴリ(すべて/携帯電話/ノートパソコン/タブレット/ヘッドホン)
- 並べ替え:価格(昇順/降順)、名前、最新順
- ページネーション:1ページあたり10件表示、ページ番号による移動機能付き
- パフォーマンス:フィルタリングおよびソート済みの計算結果をキャッシュする
- 大規模リストの最適化:1,000件のテスト項目により、スムーズな動作を保証



