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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?