- Vue.js
Vue v-forとv-ifの使い方|keyと併用時の注意・v-show違い
v-forは配列などを繰り返して要素を並べるディレクティブ、v-ifは条件が真のときだけ要素を作るディレクティブです。Vue 3ではこの2つを同じ要素に書くとv-ifが先に評価されるため、「条件で絞り込みたいときはcomputed」「要素ごとに出し分けたいときは<template v-for>で包む」の2つの形に分けて書きます。
この記事では、Vue 3.5.43で実際に動かした出力をそのまま載せながら、同じ要素に書いたときのエラー文、Vue 2.7.16との優先順位の違い、コピペで使える2つの書き換え方、keyにindexを使ったときに入力欄の内容がずれる様子、v-ifとv-showの違いを順に整理します。エラー文から直し方を引ける表と、どの書き方を選ぶかの判断表も用意しました。
v-for・v-if・key・v-showの早見表
| やりたいこと | 書き方 | ポイント |
|---|---|---|
| 配列を並べる | <li v-for="item in items" :key="item.id"> |
keyにはidなど一意で変わらない値 |
| 番号も使う | v-for="(item, index) in items" |
indexは0始まり |
| オブジェクトを並べる | v-for="(value, key, index) in obj" |
値・キー・番号の順 |
| 回数だけ繰り返す | v-for="n in 3" |
nは1から始まる |
| 条件に合う行だけ並べる | computedで絞った配列をv-forに渡す | 絞り込みはcomputedが基本形 |
| 行ごとに表示を出し分ける | <template v-for>の内側でv-if / v-else |
keyは<template>側に付ける |
| リスト全体を出す・隠す | 外側の要素にv-if | v-forと同じ要素には書かない |
| 0件のときに案内を出す | <ul v-if="list.length"> と <p v-else> |
v-elseは直後の要素に書く |
| 頻繁に表示を切り替える | v-show | DOMは残し、displayだけ切り替える |
| めったに表示しない部分 | v-if | 偽の間はDOMもコンポーネントも作らない |
| 同じ要素にv-forとv-ifを書いたとき | Vue 2(2.7.16で実行) | Vue 3(3.5.43で実行) |
|---|---|---|
| 先に評価されるもの | v-for | v-if |
| v-ifからv-forの変数を参照 | できる(行ごとに判定) | できない(変数が未定義) |
| 今回の実行結果 | 条件に合う行だけ描画 | 警告とTypeErrorが出て描画されない |
| おすすめの形 | computedまたは<template> |
computedまたは<template> |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vue v-forとv-ifの使い方|keyと併用時の注意・v-show違い」
この記事の確認環境
出力はすべて次の環境で実際に動かしたものです。
| 項目 | 内容 |
|---|---|
| Vue 3 | 3.5.43(npmで入れた最新の3系) |
| Vue 2 | 2.7.16(優先順位の比較用) |
| 実行方法 | Node.js 22.22.2 + jsdom 30.1.1 上でコンポーネントをマウントし、描画後のHTMLを出力 |
| テンプレート | 動作確認はtemplate文字列(コンパイラ入りビルド vue/dist/vue.cjs.js)で実施 |
| Lint | ESLint 10.12.0 + eslint-plugin-vue 10.11.1 |
記事中のコピペ用コードは、普段使う単一ファイルコンポーネント(SFC、.vueファイル)の形で載せています。挙動の確認は同じテンプレートをtemplate文字列として動かしたもので、SFCをViteでビルドしてブラウザで開く確認は未実行です。テンプレートのコンパイル結果は同じ仕組みで作られるため、ディレクティブの挙動は共通です。
v-forの基本の書き方
配列・オブジェクト・回数を繰り返す
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 10, name: 'ノート' },
{ id: 20, name: 'ペン' },
])
const user = ref({ name: '田中', role: 'admin' })
</script>
<template>
<!-- 配列(番号つき) -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index }}: {{ item.name }}
</li>
</ul>
<!-- 分割代入も使える -->
<ul>
<li v-for="{ id, name } in items" :key="id">{{ id }}-{{ name }}</li>
</ul>
<!-- オブジェクト(値, キー, 番号) -->
<ul>
<li v-for="(value, key, index) in user" :key="key">
{{ index }}. {{ key }} = {{ value }}
</li>
</ul>
<!-- 回数(1から始まる) -->
<span v-for="n in 3" :key="n">{{ n }} </span>
</template>
実行結果(描画されたHTML)です。
<li>0: ノート</li><li>1: ペン</li>
<li>10-ノート</li><li>20-ペン</li>
<li>0. name = 田中</li><li>1. role = admin</li>
<span>1</span><span>2</span><span>3</span>
| 書き方 | 取り出せる値 | 補足 |
|---|---|---|
item in items |
要素 | item of items と書いても同じ結果 |
(item, index) in items |
要素, 番号 | 番号は0始まり |
{ id, name } in items |
分割代入した値 | 番号つきは ({ id, name }, index) |
(value, key, index) in obj |
値, キー, 番号 | 順番は値が先 |
n in 3 |
1, 2, 3 | 0ではなく1から |
0件のときの案内はv-ifとv-elseで書く
リストが空のときに案内文を出すのは、v-forとv-ifを組み合わせる代表的な形です。v-ifは外側の要素に付け、v-forとは別の要素に分けるのがポイントです。
<template>
<ul v-if="items.length">
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
<p v-else>データがありません</p>
</template>
items が空配列のときの実行結果です。
<p>データがありません</p>
同じ要素にv-forとv-ifを書くとどうなるか
Vue 3ではv-ifが先に評価されてエラーになる
未完了のタスクだけを並べたいとき、つい次のように書きたくなります。
<script setup>
import { ref } from 'vue'
const todos = ref([
{ id: 1, name: '備品を発注する', done: false },
{ id: 2, name: '議事録を書く', done: true },
{ id: 3, name: '在庫を数える', done: false },
])
</script>
<template>
<ul>
<!-- エラーになる例:同じ要素に v-for と v-if -->
<li v-for="todo in todos" v-if="!todo.done" :key="todo.id">
{{ todo.name }}
</li>
</ul>
</template>
Vue 3.5.43での実行結果です。
[Vue warn]: Property "todo" was accessed during render but is not defined on instance.
TypeError: Cannot read properties of undefined (reading 'done')
Vue 3では同じ要素にv-forとv-ifがあると、v-ifが先に評価されます。 v-ifの時点ではまだ繰り返しが始まっていないので、todo という変数はどこにもありません。そのため「todoが定義されていない」という警告が出て、続けて undefined.done を読もうとしてTypeErrorで止まります。
コンパイルされた描画関数を出力すると、順番がはっきり見えます(要点だけ抜粋)。
return (!todo.done) // 先に v-if の条件
? (_openBlock(true), _createElementBlock(_Fragment, { key: 0 },
_renderList(todos, (todo) => { // その内側で v-for
return (_openBlock(), _createElementBlock("li", { key: todo.id }, ...))
}), 128 /* KEYED_FRAGMENT */))
: _createCommentVNode("v-if", true)
条件式 !todo.done が _renderList(v-forの本体)の外側にあるため、todo に届かないことがわかります。
Vue 2では同じコードが動く(優先順位が逆)
同じテンプレートをVue 2.7.16で動かすと、エラーは出ずに未完了の2件だけが描画されました。
<ul><li>備品を発注する</li><!----><li>在庫を数える</li></ul>
Vue 2の描画関数は次の形です。
_c('ul', _l((todos), function (todo) { // 先に v-for
return (!todo.done) // 行ごとに v-if
? _c('li', { key: todo.id }, [_v(_s(todo.name))])
: _e()
}), 0)
| 比べる点 | Vue 2.7.16 | Vue 3.5.43 |
|---|---|---|
| 優先されるディレクティブ | v-for | v-if |
| 描画関数の形 | 繰り返しの内側で条件判定 | 条件判定の内側で繰り返し |
| v-ifで行の変数を使う | 動く(全行を回してから判定) | 変数が未定義になる |
| 実行結果 | 2件描画 | 警告 + TypeError |
| 条件が行の変数を使わない場合 | 行ごとに同じ判定を繰り返す | リスト全体の表示・非表示になる |
Vue 2から移行したコードでこのエラーが出たら、優先順位が入れ替わったことが理由です。Vue 2で動いていた書き方も、全行を回してから1行ずつ判定するため、行数が多いと描画のたびに余分な処理が走ります。どちらの版でも、次の2つの形に書き換えると読みやすく、動作も安定します。
なお、Vue 3でv-ifの条件が行の変数を使わない場合(v-if="show" など)は、同じ要素に書いてもエラーにはならず、リスト全体の表示・非表示として動きました。ただし意図が読み取りにくいので、外側の要素にv-ifを移す書き方が向いています。
ESLintで書いた時点で気づく
eslint-plugin-vueの推奨設定(flat/recommended)を入れておくと、エディタやCIで気づけます。上の例を含むファイルに実行した結果の抜粋です(パスの表示は環境に合わせて置き換えています)。
/home/taro/app/App.vue
8:31 error The 'todos' variable inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if' vue/no-use-v-if-with-v-for
10:5 error Elements in iteration expect to have 'v-bind:key' directives vue/require-v-for-key
vue/no-use-v-if-with-v-for が併用を、vue/require-v-for-key がkeyの付け忘れを知らせてくれます。
書き換え方1:computedで絞り込む(コピペ用)
「条件に合う行だけ並べたい」ときは、表示用の配列をcomputedで作ってからv-forに渡します。 テンプレートには繰り返しだけが残るので読みやすく、元の配列が変わったときだけ再計算されます。
<script setup>
import { ref, computed } from 'vue'
const todos = ref([
{ id: 1, name: '備品を発注する', done: false },
{ id: 2, name: '議事録を書く', done: true },
{ id: 3, name: '在庫を数える', done: false },
])
// 未完了だけを取り出した表示用の配列
const activeTodos = computed(() => todos.value.filter((todo) => !todo.done))
</script>
<template>
<ul>
<li v-for="todo in activeTodos" :key="todo.id">{{ todo.name }}</li>
</ul>
</template>
実行結果です。
<ul><li>備品を発注する</li><li>在庫を数える</li></ul>
検索とチェックボックスで絞り込む完成形
絞り込み条件が増えても、computedの中に条件を足すだけです。件数表示と0件時の案内も付けた形です。
<script setup>
import { ref, computed } from 'vue'
const todos = ref([
{ id: 1, name: '備品を発注する', done: false },
{ id: 2, name: '議事録を書く', done: true },
{ id: 3, name: '在庫を数える', done: false },
])
const keyword = ref('')
const onlyActive = ref(true)
const visibleTodos = computed(() =>
todos.value.filter(
(todo) =>
(!onlyActive.value || !todo.done) && todo.name.includes(keyword.value),
),
)
</script>
<template>
<input v-model="keyword" placeholder="キーワード">
<label><input type="checkbox" v-model="onlyActive">未完了のみ</label>
<p>{{ visibleTodos.length }} / {{ todos.length }} 件</p>
<ul v-if="visibleTodos.length">
<li v-for="todo in visibleTodos" :key="todo.id">{{ todo.name }}</li>
</ul>
<p v-else>該当するタスクはありません</p>
</template>
実行結果(初期表示 → キーワードを「在庫」に → 「xyz」に)です。
初期表示 : 2 / 3 件 <li>備品を発注する</li><li>在庫を数える</li>
keyword=在庫 : 1 / 3 件 <li>在庫を数える</li>
keyword=xyz : 0 / 3 件 <p>該当するタスクはありません</p>
入れ子のリストは関数で絞り込む
グループごとにタスクを持つような入れ子では、内側の配列ごとにcomputedを用意できません。その場合は絞り込み用の関数を作り、内側のv-forに渡します。
<script setup>
import { ref } from 'vue'
const groups = ref([
{ id: 1, title: '今週', tasks: [
{ id: 1, name: 'A', done: false },
{ id: 2, name: 'B', done: true },
] },
{ id: 2, title: '来週', tasks: [{ id: 3, name: 'C', done: false }] },
])
const activeOf = (group) => group.tasks.filter((task) => !task.done)
</script>
<template>
<section v-for="group in groups" :key="group.id">
<h3>{{ group.title }}</h3>
<ul>
<li v-for="task in activeOf(group)" :key="task.id">{{ task.name }}</li>
</ul>
</section>
</template>
実行結果です。
<section><h3>今週</h3><ul><li>A</li></ul></section>
<section><h3>来週</h3><ul><li>C</li></ul></section>
関数は再描画のたびに呼ばれます。行数が多くて気になるときは、グループを子コンポーネントに分け、その中でcomputedを使う形にすると再計算を抑えられます。
書き換え方2:templateタグで包む(コピペ用)
「行ごとに表示を出し分けたい」「条件に合わない行は別の見た目にしたい」ときは、<template v-for> で包み、内側の要素にv-ifを書きます。 <template> は描画結果に残らない入れ物なので、余分な要素は増えません。
<template>
<ul>
<template v-for="todo in todos" :key="todo.id">
<li v-if="!todo.done">{{ todo.name }}</li>
</template>
</ul>
</template>
実行結果です。条件に合わない行の位置には、目印のコメントノードだけが入ります。
<ul><li>備品を発注する</li><!--v-if--><li>在庫を数える</li></ul>
v-elseと組み合わせると、行ごとの出し分けになります。
<template>
<ul>
<template v-for="todo in todos" :key="todo.id">
<li v-if="!todo.done">{{ todo.name }}</li>
<li v-else class="done">済: {{ todo.name }}</li>
</template>
</ul>
</template>
<ul><li>A</li><li class="done">済: B</li></ul>
keyはtemplate側に付ける
Vue 3では、<template v-for> のkeyは <template> 自体に付けます。内側の要素に付けると、次のコンパイルエラーが表示されました。
[Vue warn]: Template compilation error: <template v-for> key should be placed on the <template> tag.
1 | <template v-for="i in items"><li :key="i.id">{{ i.name }}</li></template>
| ^^^^^^^^^^^
computedとtemplateのどちらを選ぶか
| 状況 | 向いている形 | 理由 |
|---|---|---|
| 条件に合う行だけを並べたい | computed | テンプレートが繰り返しだけになり、件数表示にも使い回せる |
| 絞り込み条件が複数ある(検索・タブ・チェック) | computed | 条件をスクリプト側にまとめられる |
| 行ごとに見た目を変えたい(済・未済など) | <template v-for> + v-if / v-else |
全行を回したうえで出し分ける処理だから |
| 行の中の一部だけ条件つきにしたい | 行の内側の要素にv-if | 行そのものは常に描画する |
| リスト全体を出す・隠す | 外側の要素にv-if | 繰り返しの前に1回だけ判定する |
| 入れ子の内側を絞りたい | 関数、または子コンポーネント + computed | 内側の配列ごとにcomputedを書けないため |
迷ったときは、次の順に考えると書き方が決まります。
flowchart TD
A[v-forとv-ifを一緒に使いたい] --> B{条件は行の変数を使う?}
B -- 使わない --> C[外側の要素にv-ifを書く]
B -- 使う --> D{合わない行はどうする?}
D -- 出さない --> E[computedで絞った配列をv-forへ]
D -- 別の見た目で出す --> F[template v-forで包み 内側にv-if と v-else]
E --> G{入れ子の内側?}
G -- はい --> H[絞り込み関数 または 子コンポーネント+computed]
G -- いいえ --> I[そのままcomputed]
判断の分かれ目は「条件が行の変数を使うか」と「合わない行を出すか出さないか」の2点です。行の変数を使わない条件はリスト全体の話なので外側へ、行の変数を使う条件は絞り込みか出し分けかで形を選びます。
keyの付け方:indexを使うと入力欄の内容がずれる
keyの役割
keyは、再描画の前後で「どの行がどの行と同じものか」をVueに伝えるための目印です。Vueはリストが変わったとき、keyが同じ要素とコンポーネントをそのまま再利用し、位置だけを動かします。keyがない、または行の中身と結びつかない値だと、Vueは上から順に同じ位置の要素を使い回し、表示内容だけを書き換えます。
実行して比べる:keyなし・index・id
各行に、v-modelを付けていない入力欄(メモ)と、内部にカウンタを持つ子コンポーネントを置きました。1行目のカウンタを5にし、3行にメモを入力した状態から、先頭の「りんご」を削除し、そのあと先頭に「もも」を追加します。
<template>
<ul>
<!-- ここの :key を「なし」「index」「item.id」に変えて比べる -->
<li v-for="(item, index) in items" :key="index">
{{ item.name }}
<input placeholder="メモ">
<Row :name="item.name" />
</li>
</ul>
</template>
:key="index" の実行結果です。
[削除前]
りんご | input="りんごのメモ" | count=5
みかん | input="みかんのメモ" | count=0
ぶどう | input="ぶどうのメモ" | count=0
[先頭(りんご)を削除後]
みかん | input="りんごのメモ" | count=5
ぶどう | input="みかんのメモ" | count=0
[先頭に「もも」を追加後]
もも | input="りんごのメモ" | count=5
みかん | input="みかんのメモ" | count=0
ぶどう | input="" | count=0
「りんご」を消したのに、「みかん」の行に「りんごのメモ」とカウンタ5が残っています。 indexは「何番目か」を表すだけなので、先頭を消すとみかんが0番になり、Vueは「0番の行はそのまま使える」と判断して、名前の表示だけを書き換えたためです。入力欄の中身や子コンポーネントの内部状態は、行のデータではなく位置に付いたままになります。
:key="item.id" に変えた実行結果です。
[先頭(りんご)を削除後]
みかん | input="みかんのメモ" | count=0
ぶどう | input="ぶどうのメモ" | count=0
[先頭に「もも」を追加後]
もも | input="" | count=0
みかん | input="みかんのメモ" | count=0
ぶどう | input="ぶどうのメモ" | count=0
メモもカウンタも、行のデータと一緒に動きました。
| keyの書き方 | 先頭を削除したあとの「みかん」行 | 評価 |
|---|---|---|
| keyなし | メモは「りんごのメモ」、カウンタ5(ずれる) | 位置で使い回される |
:key="index" |
メモは「りんごのメモ」、カウンタ5(ずれる) | keyなしと同じ結果 |
:key="item.id" |
メモは「みかんのメモ」、カウンタ0(正しい) | 行のデータと状態が一致 |
keyに使う値の選び方
| 候補 | 向き・不向き | 補足 |
|---|---|---|
| データベースのid・コードなど | 向いている | 一意で、並べ替えても変わらない |
| 追加時に採番した連番やUUID | 向いている | idを持たないデータは、追加する時点で振っておく |
| 名前など重複しうる文字列 | 重複しない保証があるときだけ | 同じ値が2つあると行の対応が取れなくなる |
| index | 並び順が変わらない表示専用のリストに限る | 追加・削除・並べ替えがあると状態がずれる |
| オブジェクトそのもの | 向かない | keyは文字列か数値にする |
Math.random() など毎回変わる値 |
向かない | 再描画のたびに全行を作り直すことになる |
indexを使ってよいのは、「並びが変わらない」「行の中に入力欄や状態を持つコンポーネントがない」の両方を満たすときと覚えておくと判断しやすくなります。なお、v-modelでデータと結びつけた入力欄は、値がデータ側から書き戻されるためずれは目立ちにくくなりますが、フォーカス位置や子コンポーネントの内部状態、トランジションの対象は同じように位置に付きます。
v-ifとv-showの違い
描画結果とライフサイクルを実行して比べる
同じ子コンポーネントを、片方はv-if、もう片方はv-showで表示し、条件をfalse → true → false → trueと切り替えました。子コンポーネントには、マウント時(onMounted)とアンマウント時(onUnmounted)に記録を残す処理を入れています。
<template>
<Child v-if="on" label="v-if" />
<Child v-show="on" label="v-show" />
</template>
初期(false) HTML: <!--v-if--><p style="display: none;">v-show</p> | hooks: ["v-show: mounted"]
true に HTML: <p>v-if</p><p style="">v-show</p> | hooks: ["v-if: mounted"]
false に HTML: <!--v-if--><p style="display: none;">v-show</p> | hooks: ["v-if: unmounted"]
再び true HTML: <p>v-if</p><p style="">v-show</p> | hooks: ["v-if: mounted"]
読み取れることは3つです。
- v-ifは偽の間、要素を作りません。 HTMLにはコメントノードだけが入り、真になった時点で初めてマウントされます
- v-showは最初から要素を作り、
display: noneで隠します。 条件が偽でも初期表示の時点でmountedが呼ばれています - v-ifは切り替えのたびにunmounted → mountedが呼ばれます。 子コンポーネントの内部状態(入力途中の値など)は作り直しで初期値に戻ります。v-showでは何も呼ばれず、状態もそのまま残ります
比較表:DOM生成・初期コスト・切替コスト・ライフサイクル
| 比べる点 | v-if | v-show |
|---|---|---|
| 偽のときのDOM | 作らない(コメントノードのみ) | 作る(display: none で隠す) |
| 初期表示のコスト(偽で始まる場合) | 小さい | 表示するときと同じだけかかる |
| 切り替えのコスト | 要素の生成・破棄が走る | スタイルの切り替えだけ |
| mounted / unmounted | 切り替えのたびに呼ばれる | 最初の1回だけ |
| 子コンポーネントの内部状態 | 偽になると破棄される | 保持される |
| 偽の間のデータ取得・監視 | 動かない | 動き続ける |
| v-else / v-else-if | 使える | 使えない |
<template> に付ける |
使える | 向かない(下記の実行結果) |
| v-forと同じ要素に書く | Vue 3では行の変数を参照できない | 行ごとの表示切り替えとして動く |
2,000行のブロックで時間を測る
2,000行のリストを入れたブロックを、v-if / v-showで切り替えたときの時間を測りました。条件は偽から始め、5回測った中央値です。
| 測ったもの | v-if | v-show |
|---|---|---|
| 初期表示時点の行要素の数 | 0 | 2,000 |
| 初期描画(偽で開始) | 0.5 ms | 48.5 ms |
| 切り替え1回あたり(10回の平均) | 24.9 ms | 3.6 ms |
この数値はNode.js上のjsdomで測った参考値で、ブラウザでの値とは異なります(ブラウザでの計測は未実行)。大小関係として「初期表示はv-ifが軽く、切り替えはv-showが軽い」ことが読み取れます。
どちらを選ぶかの判断表
| 状況 | 選ぶもの | 理由 |
|---|---|---|
| タブ・アコーディオンなど何度も開閉する | v-show | 切り替えが軽く、入力途中の内容も残る |
| 権限や設定で決まり、表示中はほぼ変わらない | v-if | 不要な要素を作らずに済む |
| 初期表示では隠れている重い部品(モーダル内の表など) | v-if | 開くまで生成とデータ取得を遅らせられる |
| 閉じたら入力内容を初期化したいフォーム | v-if | 作り直しで状態が初期値に戻る |
| 閉じても入力内容を残したいフォーム | v-show | 状態が保持される |
| A / B / C の出し分け | v-if / v-else-if / v-else | v-showにはelseがない |
| リストの行ごとの出し分け | computed、または<template v-for> + v-if |
上の「書き換え方」を参照 |
v-showで気をつける2点
v-showは「その要素自身のdisplayを切り替える」仕組みなので、実体のない <template> には向きません。<template v-show="false"> を動かすと、中身の代わりに空のtemplate要素が出力されました。複数要素をまとめて切り替えたいときは、<div> などで包んでv-showを付けるか、<template v-if> を使います。
<template style="display: none;"></template><p>after</p>
また、v-showの直後にv-elseを書くと、コンパイル時に次のメッセージが出ます。
[Vue warn]: Template compilation error: v-else/v-else-if has no adjacent v-if or v-else-if.
なお、v-forと同じ要素にv-showを書いた場合は、行の変数を参照できます。全行のDOMを作ったうえで、条件に合わない行を隠す動きになります。
<ul><li>備品を発注する</li><li style="display: none;">議事録を書く</li><li>在庫を数える</li></ul>
行数が少なく、表示・非表示を頻繁に切り替える一覧では使える形です。行数が多い一覧では、computedで絞り込むほうがDOMの数を抑えられます。
エラー・警告から直し方を引く表
| 表示されるメッセージ | 起きていること | 直し方 |
|---|---|---|
Property "todo" was accessed during render but is not defined on instance. |
同じ要素のv-ifからv-forの変数を参照している | computedで絞る、または<template v-for>で包む |
TypeError: Cannot read properties of undefined (reading 'done') |
上と同じ原因で、未定義の変数のプロパティを読んでいる | 同上 |
<template v-for> key should be placed on the <template> tag. |
<template v-for> の内側の要素にkeyを付けている |
keyを<template>に移す |
v-else/v-else-if has no adjacent v-if or v-else-if. |
v-elseの直前がv-ifの要素になっていない(v-showの後など) | v-ifの直後に置く。v-showなら逆の条件のv-showを書く |
vue/no-use-v-if-with-v-for(ESLint) |
v-forとv-ifの併用 | computedまたは<template> |
vue/require-v-for-key(ESLint) |
v-forにkeyがない | 一意なidをkeyに付ける |
| メッセージは出ないが入力欄の内容が別の行に移る | keyがない、またはindexをkeyにしている | idなど行のデータに結びつく値をkeyにする |
| 開閉のたびに入力内容が消える | v-ifで要素を作り直している | 残したい場合はv-showにする |
書く前のチェックリスト
| 確認すること | 目安 |
|---|---|
| v-forとv-ifが同じ要素に並んでいないか | 並んでいたらcomputedか<template>へ |
| v-forにkeyがあるか | idなど一意で変わらない値 |
| keyにindexを使っていないか | 追加・削除・並べ替えがあるならidに変える |
<template v-for> のkeyは<template>側にあるか |
内側の要素ではなく外側に |
| 0件のときの表示を用意したか | v-if="list.length" と v-else |
| 頻繁に切り替える部分にv-ifを使っていないか | 開閉が多いならv-show |
| ほとんど表示しない重い部分にv-showを使っていないか | 初期表示を軽くするならv-if |
| eslint-plugin-vueの推奨設定を入れているか | 併用とkeyの付け忘れを自動で検出 |
よくある質問
Q. v-forとv-ifを同じ要素に書いてはいけないのですか?
A. Vue 3ではv-ifが先に評価されるため、v-ifの条件で行の変数(todo など)を使うと未定義になり、警告とTypeErrorが出ます。条件に合う行だけ並べたいならcomputedで絞った配列を渡し、行ごとに出し分けたいなら <template v-for> で包んで内側にv-ifを書くと、意図どおりに動きます。
Q. Vue 2では動いていたのに、Vue 3にしたらエラーになりました。なぜですか?
A. 優先順位が入れ替わったためです。今回の実行でも、同じテンプレートがVue 2.7.16では2件描画され、Vue 3.5.43では警告とTypeErrorになりました。Vue 2はv-forが先、Vue 3はv-ifが先です。computedまたは <template> の形に書き換えると、どちらの版でも同じ結果になります。
Q. keyにindexを使うとどうなりますか? A. 先頭の行を削除したり並べ替えたりすると、入力欄の内容や子コンポーネントの内部状態が別の行に移ります。今回の実行では、keyなしのときと同じ結果になりました。並びが変わらない表示専用のリストでは問題になりませんが、追加・削除・並べ替えがあるリストではidをkeyにします。
Q. v-ifとv-showはどう使い分けますか? A. 何度も切り替える部分はv-show、めったに表示しない部分や初期表示を軽くしたい部分はv-ifが向いています。v-ifは切り替えのたびに要素を作り直すため子コンポーネントの状態が初期値に戻り、v-showは状態を保ったままdisplayだけを切り替えます。
まとめ
- Vue 3では同じ要素のv-forとv-ifはv-ifが先に評価され、行の変数が未定義になる(Vue 2はv-forが先)
- 条件に合う行だけ並べるならcomputedで絞った配列をv-forに渡す
- 行ごとに出し分けるなら
<template v-for>で包み、内側にv-if / v-elseを書く。keyは<template>側に付ける - keyにはidなど一意で変わらない値を使う。indexはkeyなしと同じ動きになり、入力欄や子コンポーネントの状態がずれる
- v-ifは偽の間DOMを作らず、切り替えのたびにmounted / unmountedが呼ばれる
- v-showは最初からDOMを作り、displayだけを切り替えるので、頻繁な開閉に向く
- eslint-plugin-vueの推奨設定で、併用とkeyの付け忘れを書いた時点で検出できる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vue v-forとv-ifの使い方|keyと併用時の注意・v-show違い」
株式会社RJCでは、Vueのリスト表示や条件表示の書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。