Vue 3 のカスタムコンポーネントで v-model が同期しない
Vue 3 のカスタムコンポーネントの v-model は親が `:modelValue` / `@update:modelValue` を経由して受け渡しする糖衣構文。
子側で defineProps/defineEmits の宣言が抜ける、`defineModel` を 3.4 未満で使う、`v-model:foo` の名前と prop 名がずれる、で同期しない。
#v-model#defineModel#props#emit#composition-api
公開:
要約
Vue 3 のカスタムコンポーネントの v-model は :modelValue と @update:modelValue の自動展開。
子側で props と emits が宣言されていない、または 3.4+ の defineModel を旧バージョンで使うと同期しない。Vue 3.4 以降なら defineModel()、3.3 以前なら props/emits を明示 が基本形。
よくある原因
- prop / emit 未宣言: 子で
defineProps/defineEmitsを書かないと、親からの値も親への変更通知も成立しない - defineModel の早期使用:
defineModelは Vue 3.4 で安定。
3.3 以前で書くとコンパイル時に展開されず実行時に undefined を弄ることになる - 名前付き v-model の名前ずれ:
v-model:titleと書いたら子もdefineModel<string>('title')かprops.titleで受ける - 内部 ref への直結: 子で
<input v-model="localRef" />のように内部状態に繋ぎ、親への emit が無い
解決策
1. Vue 3.4+ で defineModel を使う
<script setup lang="ts">
const model = defineModel<string>();
</script>
<template>
<input v-model="model" />
</template><MyInput v-model="text" />defineModel<string>() の戻り値は ref。v-model でそのまま接続でき、値の取得・更新・親への emit を Vue がまとめて処理する。
詳細は Component v-model を参照。
2. Vue 3.3 以前は props/emits を明示
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>();
const emit = defineEmits<{
(e: "update:modelValue", value: string): void;
}>();
</script>
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
</template>3. 名前付き v-model の名前を揃える
<UserForm
v-model:first-name="form.firstName"
v-model:last-name="form.lastName"
/><script setup lang="ts">
const firstName = defineModel<string>("firstName");
const lastName = defineModel<string>("lastName");
</script>親側のケバブケース v-model:first-name は子の defineModel<string>('firstName') に対応する。
引数名と親の名前を一致させる。
4. 内部 ref に逃さない
子コンポーネント内で const local = ref(props.modelValue) と内部状態を作ってしまうと、親と同期しなくなる。defineModel を使うか、props を直接 :value バインドし @input で emit する。