🎉

Vue 3.4 v-bind Same Name ShortHand と defineModel をちょっと触る

2024/01/25に公開

Vue 3.4 の update の中でも実装にダイレクトに影響してきそうな2つを軽く触ってみる。

v-bind Same Name ShortHand

https://blog.vuejs.org/posts/vue-3-4#v-bind-same-name-shorthand

親コンポーネントから子コンポーネントに渡す値の変数名が、子コンポーネントの props のプロパティ名と一致する場合、省略した形で実装できるようになった。

<!-- ChildComponent.vue -->
<script setup lang="ts">
const props = defineProps<{ sample1: string }>();
</script>

<template>
  <div>sample1: {{ props.sample1 }}</div>
</template>
<!-- ParentComponent.vue -->
<script setup lang="ts">
import { ref } from "vue";
import ChildComponent from "./components/ChildComponent.vue";

const sample1 = ref("");
</script>

<template>
  <div class="parent">
    <!-- sample1という変数名がChildComponentのpropsと一致するので省略可能 -->
    <ChildComponent :sample1></ChildComponent>
  </div>
</template>

親コンポーネント側の変数名がキャメルケースの場合、v-bind はキャメルケースでもスネークケースでも OK。

<!-- ChildComponent.vue -->
<script setup lang="ts">
const props = defineProps<{ sampleObj1: { value1: string } }>();
</script>

<template>
  <div>sampleObj1: {{ props.sampleObj1 }}</div>
</template>
<!-- ParentComponent.vue -->
<script setup lang="ts">
import ChildComponent from "./components/ChildComponent.vue";

const sampleObj1 = reactive({ value: "" });
</script>

<template>
  <div class="parent">
    <!-- どちらでもOK -->
    <ChildComponent :sample-obj1></ChildComponent>
    <ChildComponent :sampleObj1></ChildComponent>
  </div>
</template>

親コンポーネントから受け取った props をそのまま孫コンポーネントに渡すときも同様。

<!-- ChildComponent.vue -->
<script setup lang="ts">
const props = defineProps<{ sample1: string }>();
</script>

<template>
  <GrandChildComponent :sample1></GrandChildComponent>
</template>

短くできるのはシンプルに助かるが、正直そこまで使わないかな?
個人的には、あえて別名にすることもあるし、この記法を使うためにわざわざ合わせる必要まではないと思う。

ちなみに Volar はまだこの記法に対応できていないので注意。
https://github.com/vuejs/language-tools/pull/3831

defineModel

https://blog.vuejs.org/posts/vue-3-4#definemodel-is-now-stable

コンポーネント内で model を定義するためのマクロ。
単なる糖衣構文的なものかと思っていたけど、そうでもないっぽい。

<!-- ChildComponent -->
<script setup lang="ts">
const model = defineModel();
</script>

<template>
  <div>
    {{ model }}
  </div>
</template>
<!-- ParentComponent -->
<script setup lang="ts">
import { ref } from "vue";
import ChildComponent from "./components/ChildComponent.vue";

const value = ref("");
</script>

<template>
  <div class="sample-main">
    <div><ChildComponent v-model="value"></ChildComponent></div>
  </div>
</template>

子コンポーネント内で defineModel()を宣言することで、下記の defineProps による modelValue と defineEmit による'update:model-value'がどちらも定義される...が、それだけではない。

defineModel の場合、子コンポーネント内で直接 model の値を書き換えることができる。

<!-- ChildComponent -->
<script setup lang="ts">
const model = defineModel();

const onClick = () => {
  model.value = 'modified';
}
// Vue 3.3までの実装でいうと↓こうなる?が、もちろんNG
// const props = defineProps<{ modelValue?:string }>();
// const onClick = () => {
//   props.modelValue = 'modified';
// }
</script>

<template>
  <div>
    {{ model }}
  </div>
  <button @click="onClick">/<button>
</template>

The value returned by defineModel() is a ref. It can be accessed and mutated like any other ref, except that it acts as a two-way binding between a parent value and a local one:

コンポーネント内で宣言した通常の ref と同じように扱いつつ、v-model を通して親コンポーネントの値と双方向バインディングしてくれる。
子コンポーネントで書き換えるだけでなく、子コンポーネントから input や孫となるカスタムコンポーネントに渡すこともできる。

<!-- ChildComponent -->
<script setup lang="ts">
const model = defineModel();
</script>

<template>
  <div>
    {{ model }}
  </div>
  <input v-model="model" />
  <GrandChildComponent v-model="model"></GrandChildComponent>
</template>

model を別名で定義したり、複数定義することももちろん可能。

<script setup lang="ts">
const model1 = defineModel("model1");
const model2 = defineModel("model2");
</script>

https://vuejs.org/guide/components/v-model.html#v-model-arguments

型パラメータを使うこともできるが、defineProps との地味な違いとして、defineModel の場合はデフォルトでオプショナルになる。必須にしたい場合はオプションを使う必要がある。ちょっとめんどう。
一方で、defineProps ではオプションと型パラメータを併用できないが、defineModel では、型パラメータを使いつつ引数にオプションを指定することができるようになった。default 値も渡せる。

<script setup lang="ts">
// const model = defineModel<string>(); // -> ModelRef<string | undefined>
const model = defineModel<string>({ required: true, default: "hello" }); -> ModelRef<string>
// definePropsの場合、型パラメータでオプショナルにしていなければそのまま必須になる。
// const props = defineProps<{ modelValue: string }>();
</script>

https://vuejs.org/api/sfc-script-setup.html#usage-with-typescript

という感じで、v-model を使った実装がかなり簡潔になる。
よくいわれる「バケツリレー」のめんどくささがかなり低減されると思う。provide/inject で無理やり回避してる人もいるかもしれないけど、ここまでくれば素直に v-model で書いた方がよくなりそう。

一方で、多少めんどうでも defineProps と defineEmit で愚直に定義して、子コンポーネントが状態をもたないようにする方がシンプルなのでは?と思ったりもする。勝手に同期してくれるという動きになれれば気にならなくなる?

Discussion