(vue3でのつまずき) v-modelが動かず、change は2回発火される場合の対処


vue3 を触っていて、2つのつまずきがあったので解決方をまとめました。

■つまずき1:v-modelが動かない

components/AInput.vue

<template>
    <input
        @input="$emit('input', $event.target.value)"
        @change="$emit('change', $event.target.value)"
    />
</template>

<script>
export default {
  name: "AInput",
};
</script>

このコンポーネントにv-modelでバインドする際にvue3だとうまく動かないようでした。

App.vue
<template>
  <div class="container">
    コードを入力してください。
    <AInput 
      v-model="code"
      @change="onChange"
      type="tel"
    />
    <p>コード: {{ code }}</p>
  </div>
</template>

<script>
import AInput from "./components/AInput.vue";

export default {
  name: "App",
  components: {
    AInput,
  },
  data() {
    return {
      code: '',
    };
  },
  methods: {
    onChange(v) {
      console.log('changed')
      console.log(v)
    }
  }
};
</script>

■原因 v-modelの仕様が変わった

vue3 の v-modelの仕様が、v2から変わっていました。
https://v3.ja.vuejs.org/guide/migration/v-model.html

破壊的変更: カスタムコンポーネントで使用する場合に、v-model のプロパティとイベントのデフォルト名が変更されます。
プロパティ: value -> modelValue
イベント: input -> update:modelValue
破壊的変更: v-bind の .sync 修飾子とコンポーネントの model オプションは削除され、v-model の引数に置き換えられます。
新規: 同じコンポーネントに複数の v-model バインディングが可能になりました。
新規: カスタムの v-model 修飾子を作成する機能が追加されました。

こちらの記事に詳しく記載を頂いていました。

■解決法 $emit するイベントを update:modelValue に変更

記載の通り、AInput.vue の @input の $emit するイベントを update:modelValue に変更するとv-modelが動きました。

components/AInput.vue

<template>
    <input
        @input="$emit('update:modelValue', $event.target.value)"
        @change="$emit('change', $event.target.value)"
    />
</template>

<script>
export default {
  name: "AInput",
};
</script>

■つまずき2:changeイベントが2回発火される

また同時に、changeが2回$emitされる現象が起こりました。

■原因 vue3から $emit の仕様が変わった

下記のページとRFCを見ると、コーディングの意図を明示的にするために、$emitするイベントをwhite list化することを求める変更が行われたようでした。

■解決法 $emit するイベントをemitsに記述

こちらのissueの回答の通り、AInput.vue の $emit するイベントを 事前に把握して

export default {
  emits: ["update:modelValue","change"],
} 

と記述することでchangeが2回発火されなくなりました。

components/AInput.vue

<template>
    <input
        @input="$emit('update:modelValue', $event.target.value)"
        @change="$emit('change', $event.target.value)"
    />
</template>

<script>
export default {
  name: "AInput",
  emits: ["update:modelValue", "change"],
};
</script>

以上2つのつまずきについて解決方法をまとめました。
2つ目の方は、2回イベントが$emitされるのは結構危ないので気を付けたいと思いました。

以上です。