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 や親スコープにもアクセスできる。親子構造の一覧描画に使う。
in と of
<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-for と v-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(新) |
|---|---|---|
| 配列 state | data() に配列を置く | const items = ref([]) |
| filtered list | computed: { 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 patch | Vue が同じ位置の DOM を再利用しながら更新する戦略 |
| mutation methods | 元配列を書き換えるメソッド |
| non-mutating methods | 新しい配列を返すメソッド |
| derived list | 元stateから表示用に派生させた配列 |