できない.dev

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 を明示 が基本形。

よくある原因

  1. prop / emit 未宣言: 子で defineProps / defineEmits を書かないと、親からの値も親への変更通知も成立しない
  2. defineModel の早期使用: defineModel は Vue 3.4 で安定。
    3.3 以前で書くとコンパイル時に展開されず実行時に undefined を弄ることになる
  3. 名前付き v-model の名前ずれ: v-model:title と書いたら子も defineModel<string>('title')props.title で受ける
  4. 内部 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 する。

この記事は役立ちましたか?