List Rendering - 配列とオブジェクトの反復描画

ロードマップ: Vue.js学習ロードマップ

What(何についてか)

Vue の list rendering は、v-for を使って配列・オブジェクト・整数レンジに基づく反復描画を行う仕組みである。単に一覧を表示するだけでなく、key による identity 管理、条件分岐との組み合わせ、配列更新の追跡、表示用派生配列の設計まで含めて理解する必要がある。

Why(なぜ必要か)

UI では一覧表示が極めて多い。

  • Todo 一覧
  • API から取得した検索結果
  • テーブル行
  • コメントやメニューの列挙
  • key-value 形式のメタデータ表示

このため、Vue における v-for はテンプレート記法の中核であり、computed() や reactivity の理解とも強く接続する。

How(どう動くか)

v-for の基本

const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
<li v-for="item in items">
  {{ item.message }}
</li>

item in items は、配列 items の各要素を item というローカル変数名で順に参照しながらテンプレートを展開する、という意味になる。感覚としては template 版の forEach() に近い。

index alias

<li v-for="(item, index) in items">
  {{ index }} - {{ item.message }}
</li>

index は 0 始まりの位置情報であり、表示番号やデバッグには便利だが、identity そのものではない。後述する key に index を安易に使うと、並び替えや削除時に状態ずれの原因になる。

親スコープへのアクセス

v-for の内側では、反復中のローカル変数だけでなく親スコープの値にもアクセスできる。

<li v-for="(item, index) in items">
  {{ parentMessage }} - {{ index }} - {{ item.message }}
</li>

これは JavaScript のネストした関数スコープと類似している。

destructuring

v-for の alias 部分では分割代入が使える。

<li v-for="{ message } in items">
  {{ message }}
</li>
<li v-for="({ message }, index) in items">
  {{ message }} {{ index }}
</li>

必要なプロパティだけを取り出したい時に使える。

nested v-for

<li v-for="item in items">
  <span v-for="childItem in item.children">
    {{ item.message }} {{ childItem }}
  </span>
</li>

内側の v-for は自分のローカル変数に加え、外側の item や親スコープにもアクセスできる。親子構造の一覧描画に使う。

inof

<div v-for="item of items"></div>

of も使えるが、意味は in とほぼ同じである。基本は in で問題ない。

v-for with an Object

const myObject = reactive({
  title: 'How to do lists in Vue',
  author: 'Jane Doe',
  publishedAt: '2016-04-10'
})
<li v-for="value in myObject">
  {{ value }}
</li>

オブジェクトの value を列挙できる。さらに key や index も受け取れる。

<li v-for="(value, key) in myObject">
  {{ key }}: {{ value }}
</li>
<li v-for="(value, key, index) in myObject">
  {{ index }}. {{ key }}: {{ value }}
</li>

反復順は Object.values() ベースであり、配列ほど本格的な一覧用途ではないが、メタデータ表示などに有効である。

v-for with a Range

<span v-for="n in 10">{{ n }}</span>

整数を与えると 1 から n まで繰り返す。n は 0 始まりではなく 1 始まりである。固定数の placeholder や簡易な反復で使える。

v-for on <template>

<ul>
  <template v-for="item in items">
    <li>{{ item.msg }}</li>
    <li class="divider" role="presentation"></li>
  </template>
</ul>

1 データに対して複数の兄弟要素を出したい時に使う。<template> 自体は DOM に出力されず、複数要素セットをまとめて反復できる。

v-for with v-if

同じ要素に v-forv-if を併用するのは非推奨である。公式では v-if が先に評価されるため、v-for のローカル変数を v-if 側で期待どおりに使えない場合がある。

<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo.name }}
</li>

このような書き方は避ける。どうしても構造上必要なら <template v-for> で明示的に分ける。

<template v-for="todo in todos">
  <li v-if="!todo.isComplete">
    {{ todo.name }}
  </li>
</template>

ただし本筋は以下である。

  • リストの絞り込みは computed() で先に行う
  • リスト全体の表示 / 非表示は親コンテナ側の v-if で制御する

Maintaining State with key

<div v-for="item in items" :key="item.id">
  <!-- content -->
</div>

Vue はリスト更新時に in-place patch を行い、同じ位置の DOM を再利用しようとする。これは効率的だが、フォーム入力値や child component state のような状態を含む場合、位置ベースの再利用だけでは状態ずれが起こりうる。

key は各要素の identity を Vue に教えるための special attribute である。

  • key は「何番目か」ではなく「誰か」を表す
  • 同じ v-for が生み出す兄弟要素の範囲で一意ならよい
  • グローバル一意である必要はない
  • string / number のような primitive を使う
  • object 自体を key にしない
  • index を key に使うのは、並び替えや挿入削除があるリストでは危険

<template v-for> の場合は key<template> 側に付ける。

<template v-for="todo in todos" :key="todo.name">
  <li>{{ todo.name }}</li>
</template>

v-for with a Component

<MyComponent v-for="item in items" :key="item.id" />

component 自体にも v-for は付けられる。ただし反復中の item は自動で子componentに渡らない。component は独立したスコープを持つため、props で明示的に渡す必要がある。

<MyComponent
  v-for="(item, index) in items"
  :item="item"
  :index="index"
  :key="item.id"
/>

これは component を v-for の文脈に密結合させず、再利用可能に保つための設計である。

Array Change Detection

Mutation Methods

Vue は reactive な配列に対して、以下の破壊的メソッド呼び出しを検知して更新できる。

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

これらは元配列を書き換える操作である。

Replacing an Array

filter(), concat(), slice() のような非破壊的メソッドは新しい配列を返すため、state を更新したい時は返り値を再代入する必要がある。

items.value = items.value.filter((item) => item.message.match(/Foo/))

Vue は配列を丸ごと置き換えても、重なり合う要素を賢く再利用するため、必ずしも全DOMを捨てて再描画するわけではない。

Displaying Filtered / Sorted Results

表示用の絞り込み・並び替えは、元配列を壊さずに computed() で派生配列を作るのが自然である。

const numbers = ref([1, 2, 3, 4, 5])
 
const evenNumbers = computed(() => {
  return numbers.value.filter((n) => n % 2 === 0)
})
<li v-for="n in evenNumbers">{{ n }}</li>

これは「元state」と「表示用派生値」を分離する設計であり、Vue の computed の使いどころとして非常に典型的である。

computed が扱いにくい場面、例えば nested v-for 内で引数付きの絞り込みをしたい場合は、method / 普通の関数を使うこともある。

function even(numbers) {
  return numbers.filter((number) => number % 2 === 0)
}
<ul v-for="numbers in sets">
  <li v-for="n in even(numbers)">{{ n }}</li>
</ul>

sort() / reverse() の注意

sort()reverse() は破壊的メソッドであるため、computed getter 内で直接使うと元配列を書き換えてしまう。これは computed の副作用禁止原則に反する。

return [...numbers].reverse()

のように、先にコピーしてから使う必要がある。

実務上の判断軸

  • 一覧表示の基本は v-for
  • object や range にも適用できるが、本命は配列
  • v-if と同じ要素で混ぜず、絞り込みは computed へ逃がす
  • key は原則付ける
  • key には安定した一意IDを使い、index は避ける
  • 表示用配列は非破壊的に派生させる
  • state 更新は意図が明確なら破壊的メソッドでもよい
  • computed 内では元配列を壊さない

Options API → Composition API 差分(補足)

項目Options API(旧)Composition API(新)
配列 statedata() に配列を置くconst items = ref([])
filtered listcomputed: { activeUsers() { ... } }const activeUsers = computed(() => ...)
反復要素の component 連携props は同様だが this 文脈中心<script setup> で state / computed / props の接続が近い
配列更新this.items.push(...)items.value.push(...)

Key Concepts

用語説明
v-for配列・オブジェクト・レンジに基づいてテンプレートを反復描画する directive
alias反復中の要素を受け取るローカル変数名
index反復中の位置情報。identity ではない
key各要素の identity を Vue に教える special attribute
in-place patchVue が同じ位置の DOM を再利用しながら更新する戦略
mutation methods元配列を書き換えるメソッド
non-mutating methods新しい配列を返すメソッド
derived list元stateから表示用に派生させた配列