はじめに
今回はvueのcompositionAPIにおいて、v-modelの仕様を把握することを目的としている。主に公式の記載を私なりに噛み砕いたものになるが、watcher、inject/provideについても推奨される方法について述べる。
v-modelのコンポーネントでの利用
<script setup>
import {ref} from "vue"
import CustomInput from './CustomInput.vue'
const message = ref('hello')
</script>
<template>
<CustomInput v-model="message" /> {{ message }}
</template>
このとき、v-modelはデフォルトで以下のように展開される。
<CustomInput
:modelValue="message"
@update:modelValue="newValue => message = newValue"
/>
そのため、デフォルトのprops名はmodelValueに、デフォルトのeventはupdate:modelValueとなる。
↓これはエラーになる
<script setup>
const props = defineProps(['modelValue'])
</script>
<template>
<input v-model="modelValue" />
</template>
SyntaxError: v-model cannot be used on a prop, because local prop bindings are not writable. Use a v-bind binding combined with a v-on listener that emits update:x event instead.
ローカルpropsバインディングはread onlyなので、update:を利用してemitする必要があるというエラー。
これを受けて以下のように書き直す。
<script setup>
const props = defineProps(['modelValue'])
console.log(props) //Proxy(Object) {modelValue: 'hello'}
</script>
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
これで問題なく動くようになる。
複数v-modelバインディングの利用
ここで、modelValueだとわかりにくいなーとか、複数v-modelを利用したいとか言った場合にはデフォルトのprop名を変えることができる。
<template>
- <CustomInput v-model="message" /> {{ message }}
+ <CustomInput v-model:message="message" v-model:receiver="receiver" /> To: {{ receiver }}{{ message }}
</template>
<script setup>
const props = defineProps(['message', 'receiver'])
</script>
<template>
<input :value="receiver" @input="$emit('update:receiver', $event.target.value)">
<input :value="message" @input="$emit('update:message', $event.target.value)" />
</template>
computedによる更新
子コンポーネント内で値を更新してから親に返したい場合、下記のようにsetter&getter付きcomputedを利用することができる。またこの場合にはpropsを直接渡すわけではないから、単純にv-modelが利用できる。
<script setup>
import { computed } from "vue"
const props = defineProps(['message'])
const emit = defineEmits(['update:message'])
let message = computed({
get() {
return props.message
},
set(value) {
value = value.charAt(0).toUpperCase() + value.slice(1);
emit('update:message', value)
}
})
</script>
<template>
<input v-model="message" />
</template>
カスタムmodifierの利用
基本的にはここまでのことで対処は可能だと思うのだが、さらにmodifierと呼ばれるやり方で子コンポーネントにおいて入力値を変更して親に渡す方法もある。
デフォルトのmodifierはinputではなくchangeで更新を行う.lazyや入力値をNumbern型に変換する.numberなどがある。このようなmodifierを自作してカスタムすることができる。
<template>
- <CustomInput v-model="message" /> {{ message }}
+ <CustomInput v-model:message.capitalize="message" /> {{ message }}
</template>
capitalizeはpropsのmessageModifiersnの中に、{capitalize:true}として渡される。
<script setup>
const props = defineProps({
message: String,
messageModifiers: { default: () => ({}) }
})
const emit = defineEmits(['update:message'])
console.log(props.messageModifiers) // { capitalize: true }
function emitValue(e) {
let value = e.target.value
if (props.messageModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:message', value)
}
</script>
<template>
<input :value="message" @input="emitValue" />
</template>
あんまりこれで嬉しいな〜となる用途が想像できない。結局おなじmodifierを違うv-modelの引数につけたとしても、子コンポーネントでまた個別に処理を記載しないといけない。そのため、同じ名前のmodifierでも違う機能にすることは可能であり個人的に同じmodifier使えば同じ処理になるのという直感に反する。余計ややこしい気がする。まあ処理を統一すればいいだけではあるのだけれども。
provide/injectでやってみる
ここまでv-modelによる子コンポーネントと親コンポーネントとのstateのやりとりを解説してきたが、vue3から利用可能になったinject&provideによるやり方もあるので紹介する。
<script setup>
import {ref, provide} from "vue"
import CustomInput from './CustomInput.vue'
const message = ref('hello')
provide('message', message)
</script>
<script setup>
import { inject } from 'vue'
let message = inject('message')
</script>
<template>
<input v-model="message" />
</template>
これだけ! 記載内容もスッキリ。しかしデメリットとして以下がある。
- どこでprovideされているのかたどりにくい
- どこでinjectされ、更新されているのかたどりにくい
- 上記2つより、別の場所でinject、更新されていることに気づかない可能性もある
- inject, provideはsetup()内でしか呼べない
つまりpropsによる受け渡しの方がよりどこで定義、利用されているかという流れやステータスの更新場所がより明確になるというメリットがある。また、公式では
リアクティブな値を provide / inject する場合、可能な限り、リアクティブな状態への変更を provider の内部で維持することが推奨されます。これは、提供されるステートとその可能な変更が同じコンポーネントに配置されることを保証し、将来のメンテナンスをより容易にしてくれます。
インジェクターコンポーネントからデータを更新する必要がある場合があります。そのような場合は、状態の変更を担当する関数を提供することをおすすめします:
以上のような記載もあり、推奨されていない。見た目としてはすっきりした記載になるが、使い所は限定した方がよさそう。
補足;watchしたいとき
少し横に逸れるが、watchの引数には ref(算出 ref も含む)やリアクティブなオブジェクト、getter 関数、あるいは複数のソースの配列のみであるため、props.messageをそのまま渡してもwatchはできない。watchしたい時は以下のようにgetterをセットする必要がある。
watch(
//NG: props.message 単にstrを渡すことになる
//代わりにgetterを用いる
() => props.message,
(message) => {
console.log(`message is: ${message}`)
}
)
injectで受け取ったものについてはgetter不要。
let message = inject('message')
console.log(message) //RefImpl {__v_isShallow: false, dep: Set(1), __v_isRef: true, _rawValue: 'hello', _value: 'hello'}
watch(
() => message,
(message) => {
console.log(`count is: ${message}`)
}
)
なお、以下のブログからの参照になるがそもそもprovide時にgetterで渡してしまうという手もある。
//provide側
const message = ref('hello')
provide('message', () => message.value)
//inject側
let message = inject('message')
watch(
message,
(message) => console.log(`count is: ${message}`)
)
もしくはtoRef()
を利用する。
import { toRef } from 'vue'
const props = defineProps(['message'])
let refMessage = toRef(props, 'message') //refに変換される
console.log(refMessage) //ObjectRefImpl {_object: Proxy(Object), _key: 'message', _defaultValue: undefined, __v_isRef: true}
let refMessageGetter = toRef(() => props.message)) // getter 構文 - 3.3+ で推奨
console.log(refMessageGetter) //GetterRefImpl {__v_isRef: true, __v_isReadonly: true, _getter: ƒ}
なお、このようにtoRef
で定義したpropsのrefには通常通りreadonlyの制限がかかるため、子コンポーネント内でv-modelにバインドしたりはできない。そのような操作が必要な場合には上述のようにsetter getter付きのcomputedプロパティを利用する。
余談になるがtoRef()
を使うとsetup()内で記述したreactiveオブジェクトを分割できるので便利。
setup() {
const event = reactive({ message: 'hello' })
return { ...toRefs(event) }; // リアクティブオブジェクトだけを返すときにはtoRefs(event)でも可
}
参考