🏎️

Vueの2種類の双方向データバインディングをおさらいし、defineModel()を完全に理解する。

2024/03/17に公開

双方向データバインディングやら、Vue3.4から追加されたdefineModel()で混乱してしまったので、ここに整理します。

データバインディングとは

Wikipediaの説明が一番わかりやすいです。

データバインディング(データバインド、データ結合)は、データと対象を結びつけ、データあるいは対象の変更を暗示的にもう一方の変更へ反映すること、それを実現する仕組みのことである。
引用元:ウィキペディア(Wikipedia)

双方向データバインディングとは

Vueでの双方向データバインディングとは2種類存在します。

1.親子コンポーネント間での双方向データバインディング

親コンポーネントと子コンポーネント間でのデータの変更を自動同期することです。

  • 親コンポーネントでデータを変更すれば、その変更が子コンポーネントに自動的に反映されます。
  • 子コンポーネントでデータを変更すれば、その変更が親コンポーネントに自動的に反映されます。

2.コンポーネント内での双方向データバインディング

コンポーネント内のデータとViewの間でのデータの変更を同期することです。

  • データが変更されれば、その変更がViewに自動的に反映されます。
  • View側でデータを変更すれば、その変更がデータに自動的に反映されます。

それでは次に、この2種類の双方向データバインディングについて、どうやって実装するのかご説明します。

双方向データバインディングの実装

1.親子コンポーネント間での双方向データバインディング

Vue3.4以前は、親コンポーネントと子コンポーネントの双方向データバインディングを実現するために、

  • v-model
  • defineProps
  • defineEmits
    を使って以下のような実装をしていました。
    (defineModel()を使用した双方向データバインディングについては後で説明します。)
parent.vue
<script setup>
import { ref } from 'vue'
import child from './components/child.vue' 

const text = ref('こんにちは')
</script>

<template>
    <child v-model="text" />
</template>

この時、コンポーネントに渡しているv-modelは、

<child :modelValue="text" @update:modelValue="val => text = val" />

のシンタックスシュガーであることを思い出してください。

child.vue
<script setup>
defineProps({ modelValue: String });
defineEmits(['update:modelValue']);
</script>

<template>
  <p>{{ modelValue }}</p>
  <input
    type="text"
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

子コンポーネントが直接親コンポーネントのデータ(props)を変更することは推奨されていません。
そのため、

  • defineProps:propsを定義し、親コンポーネントからデータを受け取り、
  • defineEmitで、propsを更新する手段を定義
    しています。

2.単一コンポーネント(SFC)内での双方向データバインディング

次に、コンポーネント内での双方向データバインディング以下のように実装できます。

component.vue
<script setup>
import { ref } from 'vue';

const message = ref(''); // リアクティブなデータを定義
</script>

<template>
  <input v-model="message" /> <!-- データとフォーム要素を双方向にバインド -->
</template>

View側で、inputに何かを入力すると、リアクティブなデータが変更されます。一方で、リアクティブなデータが変更されれば、その変更がinput要素に反映されます。

この時、inputに渡しているv-modelは、

フォーム入力バインディング
<input :value="message" @input="message = $event.target.value" />

のシンタックスシュガーであることを思い出してください。

defineModel()とは

Vue3.4以降、親子コンポーネント間での双方向データバインディングは、defineModel()を用いることで次のように書けるようになりました。

parent.vue
<script setup>
import { ref } from 'vue'
import child from './components/child.vue' 

const text = ref('こんにちは')
</script>

<template>
  <child v-model="text" />
</template>
child.vue
<script setup>
const text = defineModel()
</script>

<template>
  <p>{{ text }}</p>
  <input v-model="text" />
</template>

defineModel()は、リアクティブな値であるrefを返します。
このrefは、通常のrefと同じように .valueでアクセスしたり、代入することができます。
しかし、このrefは通常のrefとは次の点で異なります。

defineModel()が返すrefのvalueは、親コンポーネント上でv-modelとして渡しているデータと自動的に同期されます。
つまり、子コンポーネントでvalueを変更すると、親のデータも自動的に更新されるようになるため、1.親子コンポーネント間の双方向バインディングを実現します。

さらに、子コンポーネント内で、v-modelにrefを渡すこともできるため、前述したように2.単一コンポーネント(SFC)内での双方向データバインディングも実現できます。

Discussion