Component v-model - 親子双方向バインディングの契約

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

What(何についてか)

Vue のコンポーネントに対して v-model を適用し、親コンポーネントの state と子コンポーネント内部の入力値を双方向に同期するための仕組みを扱う。Vue 3.4 以降では defineModel() により、この契約を ref のような形で扱える。

Why(なぜ必要か)

フォーム入力コンポーネントや再利用 UI 部品では、親が値を保持しつつ、子がその値を表示・更新できる必要がある。v-model を使うと、単なる props の受け渡しだけでなく、更新通知まで含めた一貫した API を設計できる。

How(どう動くか)

Basic Usage

defineModel() は、親から v-model で渡された値を子コンポーネント内で ref として扱うためのコンパイラマクロである。

<script setup lang="ts">
const model = defineModel<number>({ required: true })
</script>
 
<template>
  <input v-model="model" />
</template>

親側では次のように state を保持する。

<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'
 
const countModel = ref(0)
</script>
 
<template>
  <Child v-model="countModel" />
</template>

このとき初期値は子ではなく親が持つ。子の model.value を更新すると、親の countModel も更新される。

Under the Hood

defineModel() は実体として prop + emit の組み合わせに展開される。

<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
 
<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

親の <Child v-model="foo" /> は概念的には次のように解釈される。

<Child
  :modelValue="foo"
  @update:modelValue="$event => (foo = $event)"
/>

v-model は双方向バインディングに見えるが、実体は一方向のデータ受け渡しと更新通知の往復である。

defineModel のオプション

defineModel() には requireddefault を指定できる。

const model = defineModel<number>({ required: true })
const optionalModel = defineModel<number>({ default: 1 })

ただし default を子側に持たせ、親が値を渡さない場合は、親は undefined、子は既定値という非同期状態が起きうる。そのため初期値は親が持つ方が安全である。

v-model Arguments

デフォルトの modelValue 以外の名前で双方向バインディングを作る場合は、引数付き v-model を使う。

親側:

<MyComponent v-model:title="bookTitle" />

子側:

<script setup lang="ts">
const title = defineModel<string>('title', { required: true })
</script>

この契約は title prop と update:title event に対応する。

Multiple v-model Bindings

1つのコンポーネントに複数の双方向バインディングを持たせることもできる。

<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>
<script setup lang="ts">
const firstName = defineModel<string>('firstName', { required: true })
const lastName = defineModel<string>('lastName', { required: true })
</script>

defineProps() のように1つのオブジェクトへまとめるのではなく、defineModel() は 1 呼び出しごとに 1 系統の双方向契約を表す。

Handling v-model Modifiers

modifier は「値が変わった後に監視する」のではなく、「親に返す直前の値を加工する」ために使う。

<script setup lang="ts">
const [model, modifiers] = defineModel<string>()
 
function onInput(value: string) {
  if (modifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  model.value = value
}
</script>
 
<template>
  <input
    :value="model"
    @input="onInput(($event.target as HTMLInputElement).value)"
  />
</template>

この用途では watch()watchEffect() は通常不要である。modifier は副作用ではなく、更新前の変換ルールに近いため、入力ハンドラや setter 側で処理する方が責務が明確になる。

Modifiers for v-model with Arguments

引数付き v-model にも modifier を付けられる。

<MyComponent v-model:title.capitalize="myText" />
<script setup lang="ts">
const [title, titleModifiers] = defineModel<string>('title')
</script>

複数 v-model がある場合も、それぞれに独立した modifier 群を持つ。

<UserName
  v-model:first-name.capitalize="first"
  v-model:last-name.uppercase="last"
/>

Options API → Composition API 差分(補足)

項目Options API(旧)Composition API(新)
基本実装props: ['modelValue'], emits: ['update:modelValue'] を明示defineModel() で同等の契約を簡潔に宣言
値の扱いmodelValue をそのまま使う、または writable computed を定義ref と同じ感覚で model.value を扱う
modifier 対応modelModifiers などの prop を明示的に受けるconst [model, modifiers] = defineModel() で取得
複数 v-modelpropsemits を個別に定義defineModel('name') を回線ごとに宣言

Key Concepts

用語説明
defineModel()コンポーネント v-modelref として扱うためのコンパイラマクロ
modelValue引数なし v-model のデフォルト prop 名
update:modelValue引数なし v-model の更新通知 event 名
v-model:titletitle prop / update:title event を使う名前付き双方向バインディング
modifiercapitalize など、子が更新直前の値変換ルールとして扱うフラグ

全体像

flowchart LR
  P["Parent state ref"] -->|prop| C["Child defineModel()"]
  C -->|update event| P
  C --> I["Native input"]
  I -->|input value| C

実務上の判断軸

  • 初期値は原則として親が持つ。
  • v-model は値同期の仕組みであり、副作用処理のために watch() を混ぜない。
  • 複数 v-model は便利だが、回線が増えるほどコンポーネント責務も重くなるため、本当に 1 つの UI 部品として自然かを確認する。