🎯

VueのdefineModelが便利だった

2025/02/03に公開

Vue3.4から安定となったdefineModel。使用する機会があったので使ってみたら思ったより便利だったので共有!

defineModelとは?

defineModel は、Vue 3 で defineProps と defineEmits を組み合わせた新しい構文です。これを使うと、親コンポーネントからデータを受け取り、そのデータを操作しながら親コンポーネントに変更を通知する仕組みを簡潔に実現できます。
とても簡単に言うと、emitしなくなくてよくなります。

なぜ defineModel が必要なのか?

Vueで双方向バインディング(親と子コンポーネント間でデータを共有・同期)を実現するには、次のように少し手間のかかるコードを書く必要がありました。

  • 親コンポーネントからデータを渡すために props を使う。
  • 子コンポーネントから親に変更を伝えるために emits を使う。

これを手作業で行うと、コードが複雑になりがちです。defineModel を使うと、このプロセスをもっと簡単に書けます。

defineModel の基本的な使い方

defineModel は次のように使います。

<script setup lang='ts'>
const modelValue = defineModel<string>({ required: false, default: '初期値' });
</script>

<template>
  <input v-model="modelValue" />
</template>
  • defineModel の役割
    • defineModel を使うと、親コンポーネントから v-model で渡された値を簡単に受け取ることができます。
    • props や emit を明示的に定義せずに、双方向バインディングを実現できます。
  • デフォルト値の指定
    • { required: false, default: '初期値' } を指定することで、親コンポーネントが v-model で値を渡さなかった場合でも '初期値' が使用されます。
    • default を指定すると、値が undefined の場合にその値が適用されます。
  • modelValue の動作
    • 親コンポーネントが <ChildComponent v-model="value" /> のように記述すると、value の変更は modelValue に反映されます。
    • 子コンポーネントで modelValue を変更すると、内部的に emit('update:modelValue', newValue) が実行され、親に変更が通知されます。

従来の方法と比較

defineModel を使わない場合、デフォルト値を指定するには以下のように明示的に props を定義する必要がありました。

// 従来の方法
<script setup lang="ts">
const props = defineProps<{ modelValue?: string }>();
const emit = defineEmits<{
  (e: "update:modelValue", value: string): void;
}>();

const modelValue = computed({
  get: () => props.modelValue ?? '初期値',
  set: (value: string) => emit("update:modelValue", value),
});
</script>

<template>
  <input v-model="modelValue" />
</template>
// defineModel を使った方法
<script setup lang='ts'>
const modelValue = defineModel<string>({ required: false, default: '初期値' });
</script>

<template>
  <input v-model="modelValue" />
</template>

オブジェクトの発火条件に注意

このdefineModel、オブジェクトを使用する場合は注意点があるので気をつけてください。

オブジェクトの場合、キーの値を変更するだけでは発火しないです。
理由としてはVue のリアクティビティが関係しています。Vueのprops(親から渡される値)は読み取り専用です。defineModel も、ベースは propsであり、それをラップしているだけなので、渡されたオブジェクトのプロパティを直接変更しても親に通知されません。

なので下記に親に通知されるパターンされないパターンを記述します。

<script setup lang='ts'>

interface User {
  id: number;
  name: string;
}

const modelValue = defineModel<User>({ required: true });

// NG🙅‍♀️
modelValue.value.push({id: 2, name: 'Jane',})

// OK🙆‍♀️
modelValue.value = [
    ...modelValue.value,
    {
      id: 2,
      name: 'Jane',
    },
];

// OK🙆‍♀️
const updatedValue = JSON.parse(JSON.stringify(modelValue.value));
updatedValue.push({
  id: 2,
  name: 'Jane',
});
modelValue.value = updatedValue;

// NG🙅‍♀️
modelValue.value.splice(
  modelValue.value.length,
  0,
  { id: 2, name: 'Jane' }
);
</script>

ぜひVueを使用しているプロダクトがあれば検討してみてください!

株式会社find | 落とし物クラウド

Discussion