vue3のv-modelのコンポーネント利用とprovide/inject

# はじめに
今回はvueのcompositionAPIにおいて、v-modelの仕様を把握することを目的としている。主に公式の記載を私なりに噛み砕いたものになるが、watcher、inject/provideについても推奨される方法について述べる。

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


# v-modelのコンポーネントでの利用

```App.vue
<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はデフォルトで以下のように展開される。
```js
<CustomInput
  :modelValue="message"
  @update:modelValue="newValue => message = newValue"
/>
```
そのため、デフォルトのprops名はmodelValueに、デフォルトのeventはupdate:modelValueとなる。

↓これはエラーになる
```CustomInput.vue
<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する必要があるというエラー。
これを受けて以下のように書き直す。

```CustomInput.vue
<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名を変えることができる。
```diff_vue
<template>
-  <CustomInput v-model="message" /> {{ message }}
+  <CustomInput v-model:message="message" v-model:receiver="receiver" /> To: {{ receiver }}{{ message }} 
</template>
```

```CustomInputs.vue
<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が利用できる。

```CustomInput.vue
<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を自作してカスタムすることができる。

```diff_vue
<template>
-  <CustomInput v-model="message" /> {{ message }}
+  <CustomInput v-model:message.capitalize="message" /> {{ message }} 
</template>
```
capitalizeはpropsのmessageModifiersnの中に、{capitalize:true}として渡される。

```CustomInputs.vue
<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によるやり方もあるので紹介する。

```App.vue
<script setup>
import {ref, provide} from "vue"
import CustomInput from './CustomInput.vue'

const message = ref('hello')
provide('message', message)
</script>
```

```CustomInput.vue
<script setup> 
import { inject } from 'vue'
let message = inject('message')
</script>

<template>
  <input v-model="message" />
</template>
```

これだけ！　記載内容もスッキリ。しかしデメリットとして以下がある。
* どこでprovideされているのかたどりにくい
* どこでinjectされ、更新されているのかたどりにくい
* 上記２つより、別の場所でinject、更新されていることに気づかない可能性もある
* inject, provideはsetup()内でしか呼べない

つまりpropsによる受け渡しの方がよりどこで定義、利用されているかという流れやステータスの更新場所がより明確になるというメリットがある。また、公式では
> リアクティブな値を provide / inject する場合、可能な限り、リアクティブな状態への変更を provider の内部で維持することが推奨されます。これは、提供されるステートとその可能な変更が同じコンポーネントに配置されることを保証し、将来のメンテナンスをより容易にしてくれます。
インジェクターコンポーネントからデータを更新する必要がある場合があります。そのような場合は、状態の変更を担当する関数を提供することをおすすめします:

以上のような記載もあり、推奨されていない。見た目としてはすっきりした記載になるが、使い所は限定した方がよさそう。

## 補足；watchしたいとき
少し横に逸れるが、watchの引数には ref（算出 ref も含む）やリアクティブなオブジェクト、getter 関数、あるいは複数のソースの配列のみであるため、props.messageをそのまま渡してもwatchはできない。watchしたい時は以下のようにgetterをセットする必要がある。
````js
watch(
  //NG: props.message  単にstrを渡すことになる
  //代わりにgetterを用いる
  () => props.message,
  (message) => {
    console.log(`message is: ${message}`)
  }
)
````
injectで受け取ったものについてはgetter不要。
````js
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で渡してしまうという手もある。

https://zenn.dev/miyanokomiya/articles/f9737b419bb070

````js
//provide側
const message = ref('hello')
provide('message', () => message.value)

//inject側
let message = inject('message')
watch(
  message,
  (message) => console.log(`count is: ${message}`)
)
````
もしくは`toRef()`を利用する。

https://ja.vuejs.org/api/reactivity-utilities.html#toref

```js
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オブジェクトを分割できるので便利。
```js
setup() {
   const event = reactive({　message: 'hello'　})
   return { ...toRefs(event) }; // リアクティブオブジェクトだけを返すときにはtoRefs(event)でも可
}
```


## 参考

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

https://vuejs.org/guide/components/provide-inject.html#working-with-reactivity

https://ja.vuejs.org/guide/essentials/watchers.html#basic-example

https://www.vuemastery.com/courses/from-vue2-to-vue3