7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-05-28

はじめに

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

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はデフォルトで以下のように展開される。

<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名を変えることができる。

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

<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され、更新されているのかたどりにくい
  • 上記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)でも可
}

参考

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?