皆様いかがお過ごしでしょうか。
いろいろと大変な時期ですが頑張っていきましょう。
Vue 3.0 のリリースを目前に控え、今後メインの記法となっていくであろう Composition API における親子コンポーネント間のデータのやりとりにフォーカスについてまとめてみました。
※ 2020 年 4 月現在、 vue@2.6.11
+ @vue/composition-api@0.5.0
での挙動を元に書いています。
デモサイトを用意してあります。記事の該当する部分と交互に見ていただくと理解しやすいかもしれません。
https://vue-props-samples.netlify.app/
※ 記事内のコードは、装飾用のクラスなどを省略しています。
※ ブラウザ拡張機能 Vue.js devtools を使うと、コンポーネント内の様子などを確認できるようにビルドしてあります。
※ ソースはこちら。 https://github.com/jay-es/vue-props-samples
TL;DR
従来の記法 Options API に精通している方向けに一言でまとめると、
親のテンプレートと子の props オプションは今までと同じ。子の setup 関数のひとつめの引数にも props が入っている(ただし分割代入は NG)
では、順を追って詳しく解説していきます。
1. props
: 親から子にデータを渡す
まずは一番基本の、子コンポーネントへのデータの渡し方を説明していきます。
親コンポーネントの書き方
親コンポーネントのテンプレート内で、子コンポーネントのカスタム属性に変数を渡します。
:title="foo"
は子コンポーネントの title
というプロパティに foo
変数の中身(下の例の場合は abc
)を渡す、という意味です。
※ v-bind:title="foo"
とも書けますが、本記事では省略記法を使用していきます。
※ テンプレートの書き方は従来(Options API)と変わりません。
※ 親の変数名と子のプロパティ名が同じだと分かりづらいため、あえて別々にしています。
<template>
<div>
<Child :title="foo" :count="bar" />
</div>
</template>
<script>
import { ref } from '@vue/composition-api'
import Child from './child.vue'
export default {
components: { Child },
setup () {
const foo = ref('abc')
const bar = ref(123)
return { foo, bar }
}
}
</script>
子コンポーネントで親からのデータを受け取る方法
一番シンプルなのは、コンポーネントオプションの props
にプロパティ名の配列を指定する方法です。
<template>
<div>
title: {{ title }}<br />
count: {{ count }}<br />
</div>
</template>
<script>
export default {
props: ['title', 'count'] // プロパティ名の配列
}
</script>
この方法は簡単ではあるものの、他の開発者(や半年後の自分)が見たときにどのような値が渡ってくるのか分かりづらいため、避けたほうがよいです。
かわりに、オブジェクト形式にして「キーにプロパティ名、値に変数の型(コンストラクタ)」のように指定したり、
export default {
props: {
title: String, // プロパティ名: 型
count: Number
}
}
もう 1 階層ネストしたオブジェクトにして type
で型を指定し、required
(必須かどうかの真偽値)や、default
(省略された場合の値)などを指定するとよいでしょう。
export default {
props: {
title: { // プロパティ名
type: String, // 型
required: true // 必須かどうか
},
count: {
type: Number,
default: 0 // 親から値が渡されなければ 0 になる
}
}
}
※ デフォルト値を関数で生成したり、値のバリデーション関数を定義することもできます。詳細は Vue 公式サイト: プロパティのバリデーション を参照してください。
setup 関数で props を使う
Composition API の setup
関数では、ひとつめの引数で props
を取得できます。
親コンポーネントで値が変わったら props
にも反映される、というリアクティブな性質をもつオブジェクトです。関数内で使用する場合は computed
や watch
で監視する必要があります。
import { computed } from '@vue/composition-api'
export default {
props: {
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
}
},
setup (props) {
const doubleCount = computed(() => props.count * 2)
return { doubleCount }
}
}
ただし、引数を分割代入で取得してしまうと、リアクティブではなくなってしまうので注意が必要です。
(自分はたまに忘れてやってしまいます。最初はちゃんと表示されるので気づきにくいんですよね……)
setup ({ count }) {
// 親コンポーネントで値が変わっても、子コンポーネントは初期値のまま変わらない
const doubleCount = computed(() => count * 2)
TypeScript で型の情報をつける
TypeScript の場合、props
にプリミティブな型(String
など)を指定した場合はちゃんと型がつきますが、Array
や Object
を指定した場合は中身の情報がないので補完されません。
import { defineComponent } from '@vue/composition-api'
export default defineComponent({
props: {
names: {
type: Array, // 本当は文字列の配列
required: true
},
staff: {
type: Object, // 本当はユーザー定義型
required: true
}
},
setup (props) {
type Names = typeof props['names'] // -> unknown[] になってしまう
type Staff = typeof props['staff'] // -> { [key: string]: any } になってしまう
}
})
PropType
という型関数が用意されていますので、それを使ってキャストすることで型の補完が効くようになります。
import { defineComponent, computed, PropType } from '@vue/composition-api'
import { Person } from './Person'
export default defineComponent({
props: {
names: {
type: Array as PropType<string[]>, // PropType で型の情報を付与
required: true
},
staff: {
type: Object as PropType<Person>, // PropType で型の情報を付与
required: true
}
},
setup (props) {
type Names = typeof props['names'] // -> string[] と認識される
type Staff = typeof props['staff'] // -> Person 型と認識される
}
})
※ vue
パッケージも PropType
を export しているので、 Options API でも使えます。
※ 今回の例では PropType
を使わず、 Array as () => string[]
のように書いても同様の効果を得られます。
※ ちなみに、コンポーネントを作る関数名は以前 createComponent
でしたが、@vue/composition-api@0.4
から defineComponent
に変更されています。古いバージョンからアップデートした場合、 createComponent
のままでも動きますが、コンソールに下記のエラーが表示されます。
`createComponent` has been renamed to `defineComponent`.
2. emit
: 子から親にイベントを発生させる
次は子から親へデータを渡す方法です。
props
のように直接データを渡す方法は用意されていないので、イベントを通じてデータを送ります。
子コンポーネントでイベントを発生させる方法
Composition API の setup
関数のふたつめの引数に context
というオブジェクトが渡されてきます。これは従来(Options API)の this
に入っていたプロパティやメソッドの一部が格納されています。
context.emit(eventName)
を実行することで、カスタムイベントを発生させることができます。
<template>
<div>
<button @click="handleClick">Click me!</button>
</div>
</template>
<script>
export default {
setup (props, context) {
const handleClick = () => {
context.emit('my-event')
}
return { handleClick }
}
}
</script>
※ props
と違い、context
は分割代入しても悪影響はありません。
export default {
setup (props, { emit }) {
const handleClick = () => {
emit('my-event')
}
return { handleClick }
}
}
また、 emit
には任意の数の引数を渡すことができます。
export default {
setup (props, { emit }) {
const handleClick = () => {
emit('my-event', 123, 'abc', false)
}
return { handleClick }
}
}
親コンポーネントで子のイベントを受け取る方法
通常のクリックイベントなどと同じように、テンプレート内で @
もしくは v-on
ディレクティブを使います。
<template>
<div>
<Child @my-event="handleEvent" />
</div>
</template>
<script lang="ts">
import Child from './child.vue'
export default {
components: { Child },
setup () {
const handleEvent = () => {
alert('イベント発生!')
}
return { handleEvent }
}
}
</script>
子コンポーネントの emit
で 2 つ以上の引数を指定した場合は、イベントハンドラの引数として受け取ることができます。
emit('my-event', 123, 'abc', false)
export default {
components: { Child },
setup () {
const handleEvent = (...args) => {
alert(args) // -> 123, 'abc', false
}
return { handleEvent }
}
}
※ ちなみに親のテンプレートで @my-event="handleEvent($event)"
とすると、イベントハンドラには emit
の第 2 引数のみ(上記の場合は 123
)が入ってきます。
3. 双方向バインディング
さて、子に渡した親の変数を書き換えたい場合はどうしたらよいでしょう。
子コンポーネントの中で props
の中身を直接変更しようとすると、以下のようにエラーになってしまいます。
export default {
props: {
count: Number
},
setup (props) {
// ボタンのイベントハンドラ
const handleClick = () => {
props.count += 1
/*
* 以下のエラーが発生(改行は筆者が追加)
* Avoid mutating a prop directly since the value will be overwritten
* whenever the parent component re-renders.
*/
}
return { handleClick }
}
そこでイベントを使います。
子コンポーネントでイベントを発生させ、その引数を(親コンポーネントの)イベントハンドラ内で代入する、という手順を踏むことで親コンポーネントの変数の値を変更できます。
export default {
props: {
count: Number
},
setup (props, { emit }) {
// ボタンのイベントハンドラ
const handleClick = () => {
emit('my-event', props.count + 1)
}
return { handleClick }
}
}
<template>
<div>
<!-- 上と下 どちらの書き方でもよい -->
<Child :count="num" @my-event="num = $event" />
<Child :count="num" @my-event="newVal => num = newVal" />
<!-- setup 内で作った関数を渡して、その中で更新するのもあり -->
<Child :count="num" @my-event="handleEvent" />
</div>
</template>
<script>
import { ref } from '@vue/composition-api'
import Child from './child.vue'
export default {
components: { Child },
setup () {
const num = ref(0)
// イベントハンドラ内で num の値を更新
const handleEvent = (newVal) => {
num.value = newVal
}
return { num, handleEvent }
}
}
</script>
ただ、これだといちいち代入の処理を書かないといけないので少し大変です。
短くかけるシンタックスシュガーが 2 種類用意されています。
3-1. v-model
まずは古参の v-model
から。
親コンポーネント側は v-model
というディレクティブに変数を入れるだけで準備完了です(input タグなどと同じ)。
先程に比べると、相当シンプルですね。
<Child v-model="num" />
子コンポーネントでは value
という名前でプロパティが渡されてきます。更新の際は input
イベントを発生させます。
export default {
props: {
value: Number
},
setup (props, { emit }) {
// ボタンのイベントハンドラ
const handleClick = () => {
emit('input', props.value + 1)
}
return { handleClick }
}
}
つまり、v-model
は以下と同等です。
<Child :value="num" @input="num = $event" />
※ 当記事と直接関係ないですが、 v-model
について深く知りたい場合は、先日公開された Vue.jsの双方向バインディング再入門 という記事がとても参考になります。
3-2. .sync
さて、もうひとつは Vue 2.3 で加わった .sync
修飾子です。
親コンポーネント側はプロパティを渡す際、後ろに .sync
を付け足します。
これも属性がひとつだけなので、すっきりしてますね。
<Child :count.sync="num" />
子コンポーネントでのプロパティの受け取り方は通常どおりです。更新の際は update:プロパティ名
のイベントを発生させます。
export default {
props: {
count: Number
},
setup (props, { emit }) {
// ボタンのイベントハンドラ
const handleClick = () => {
emit('update:count', props.count + 1)
}
return { handleClick }
}
}
したがって、 .sync
修飾子は、以下の省略形ということになります。
<Child :count="num" @update:count="num = $event" />
3-3. Vue 3.0 での変更点
来たる Vue 3.0 では、上記の .sync
修飾子は廃止され、 v-model
が引数を取れるようになるそうです。
Instead of:
<MyComponent v-bind:title.sync="title" />the syntax would be:
<MyComponent v-model:title="title" />
<MyComponent v-model="xxx" />
<!-- would be shorthand for: -->
<MyComponent
:model-value="xxx"
@update:model-value="newValue => { xxx = newValue }"
/>
<MyComponent v-model:aaa="xxx"/>
<!-- would be shorthand for: -->
<MyComponent
:aaa="xxx"
@update:aaa="newValue => { xxx = newValue }"
/>
これを見ると、子コンポーネント側の受け取り方と更新方法は今までの .sync
修飾子のやり方に統一されていますね。
v-model
に引数がない場合は model-value
というプロパティが渡されるので、update:model-value
イベントで更新します。
3-4. アンチパターン
プリミティブでない値(配列やオブジェクトなど)を渡した場合は、子コンポーネントから直接中身を変更できてしまいます。
<template>
<div>
<Child :obj="foo" :arr="bar" :dt="baz" />
</div>
</template>
<script>
import { ref } from '@vue/composition-api'
import Child from './child.vue'
export default {
components: { Child },
setup () {
const foo = ref({
num: 0
})
const bar = ref([])
const baz = ref(new Date())
return { foo, bar, baz }
}
}
</script>
export default {
props: {
obj: Object,
arr: Array,
dt: Date
},
setup (props) {
// ボタンのイベントハンドラ
const handleClick = () => {
props.obj.num += 1 // 親コンポーネントの foo.num に反映される
props.arr.push(0) // 親コンポーネントの bar の要素が増加する
props.arr[0] += 1 // 配列内の値を変更することもできる
props.dt.setDate(Math.random()) // 日付が変わる
}
return { handleClick }
}
}
イベントの処理を書く手間が省けるので便利に感じるかもしれませんが、どこで値を変えているのかが追いづらく、メンテナンス性が著しく下がってしまうので避けたほうがよいです(経験談)。
子はイベントを発生させるだけにして、親の変数の更新は親の中のみで行ないましょう。
まとめ
Vue.js の新しい記法、 Composition API での親子コンポーネント間で直接データを受け渡しする方法について紹介しました。
冒頭にもまとめましたが、従来の書き方を知っている場合はそれほど大きな変更点はありませんね。setup
関数内での使い方も一度知ってしまえば難しくはないです。ただ分割代入には気をつけましょう。
以前 Vue.js 公式サイトで、親子のコンポーネントの関係は props down, events up
という言葉で説明されていました。[^props down] このページで説明してきた内容を端的に表している言葉ですね。
[^props down]: GitHub をさかのぼってみたら、2 年前の大きな改訂 で消えていました。つい最近見た気がするけど、そんなに前だったとは……。
今回の主旨から外れますが、親子間以外でもデータを共有したい場合は store パターン を試してみたり、公式の状態管理ライブラリ Vuex の導入を検討するとよいでしょう。
Vue 3.0 は Composition API 以外にも新機能がたくさんあるので待ち遠しいですね。
それでは。