🍕

(3.4)VueのdefineModel()って美味しいの??

2023/10/12に公開

コンパイラマクロですが、そのままでは使えないので設定が必要です。
Nuxt.jsだとnuxt.config.tsをこんな感じに設定したら使えるようになりました。

export default defineNuxtConfig({
    vite: {
        vue: {
            script: {
                defineModel: true,
                propsDestructure: true,
            },
        },
    },
});

書き方

基本

defineProps()defineEmits()が一つになっているのでコードをかなり短縮できる印象です。

// 型指定なし
const modelValue = defineModel()
// Ref<any>

// 型指定あり
const modelValue = defineModel<string>()
// Ref<string | undefined>
const modelValue = defineModel<string>({ required: true })
// Ref<string | undefined>

// 別名
const count = defineModel<number>('count')
// デフォルトの値を指定
const count = defineModel<number>('count', { default: 0 })

親でv-modelを指定しない場合に対応

オプションのlocalをtrueにすることで、親コンポーネントでv-modelが記述されなかった場合でも正しく動作させることが出来ます。もし、v-modelを指定せず、localもfalseだった場合、Refは返されますがv-modelによるバインディングは動作しませんでした。

const modelValue = defineModel<number>({ local: true });
// Ref<number | undefined>

const modelValue = defineModel<number>({ local: true, default: 0 });
// Ref<number>

ポイント

直接変更できるRef

defineModel()defineProps()とは返り値が異なります。
defineProps()は値そのものを返すのに対して、defineModel()は受け取った値を直接変更できるRefを返します。

const modelValue = defineModel<string>({ required: true });
// Ref<string>

const { modelValue } = defineProps<{ modelValue: string }>();
// string

使用例

親のコンポーネントはこれまでの記述と変わりありませんが、子コンポーネント側の\<script\>内での記述が1行に収まり、v-modelを使うことでイベントを発行することも無くなったので\<template\>内の記述もシンプルになりました。

Parent.vue
<script lang="ts" setup>
const color = ref<string>(); // Ref<string | undefined>
</script>
<template>
    <div class="p-10">
        <StyledSelect v-model="color" />
        <div>parent -> {{ color || '未選択' }}</div>
    </div>
</template>
Child.vue
<script lang="ts" setup>
const modelValue = defineModel<string>();  // Ref<string | undefined>
</script>
<template>
    <div>
        <select v-model="modelValue" class="border p-2 rounded">
            <option disabled :value="undefined">選択してください</option>
            <option value="red"></option>
            <option value="bule"></option>
            <option value="green"></option>
            <option value="white"></option>
            <option value="black"></option>
        </select>
        <span>child -> {{ modelValue }}</span>
    </div>
</template>


これまでのdefineProps()defineEmits()を使った挙動と同じですね。

{local: true}も試してみる

親側でv-modelを削除し、子側でlocalをtrueに、デフォルトの値を設定しました。

Parent.vue
<script lang="ts" setup>
const color = ref<string>();
</script>
<template>
    <div class="p-10">
        <!-- v-model削除 -->
        <StyledSelect />
        <div>parent -> {{ color || '未選択' }}</div>
    </div>
</template>
Child.vue
<script lang="ts" setup>
const modelValue = defineModel<string>({ local: true, default: 'red' });
// local -> true
// default -> 'red'
</script>
<template>
    <div>
        <select v-model="modelValue" class="border p-2 rounded">
            <option disabled :value="undefined">選択してください</option>
            <option value="red"></option>
            <option value="bule"></option>
            <option value="green"></option>
            <option value="white"></option>
            <option value="black"></option>
        </select>
        <span>child -> {{ modelValue }}</span>
    </div>
</template>

v-modelを削除したので親子で値を共有していません。しかし、defineModel()Refを返すことによって子側で独立したステートが定義されているのが確認出来ます。

まとめ

defineModel()自体はコード量をかなり短くできるので良いと思います。
しかし、既にdefineProps()とdefineEmits()がありますし、TypeScriptだと、defineProps()にジェネリックで型定義を渡せる上にwithDefaults()でデフォルト値も定義することが可能です。他にも、双方向バインディングに関しては子側でcomputed()を使った実装パターンもあります。まだ実験的な機能ですが、実装方法が増えると混乱しやすくなるポイントが増えるので今後のアプデに注目したいですね。

Discussion