Vue.js の Composition API における親子コンポーネント間のデータ受け渡し

皆様いかがお過ごしでしょうか。
いろいろと大変な時期ですが頑張っていきましょう。

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://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) を使うと、コンポーネント内の様子などを確認できるようにビルドしてあります。
※ ソースはこちら。 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）と変わりません。
※ 親の変数名と子のプロパティ名が同じだと分かりづらいため、あえて別々にしています。

```vue:親コンポーネント
<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` にプロパティ名の配列を指定する方法です。

```vue:子コンポーネント
<template>
  <div>
    title: {{ title }}<br />
    count: {{ count }}<br />
  </div>
</template>

<script>
export default {
  props: ['title', 'count'] // プロパティ名の配列
}
</script>
```

この方法は簡単ではあるものの、他の開発者（や半年後の自分）が見たときにどのような値が渡ってくるのか分かりづらいため、避けたほうがよいです。

かわりに、オブジェクト形式にして「キーにプロパティ名、値に変数の型（コンストラクタ）」のように指定したり、

```javascript
export default {
  props: {
    title: String,  // プロパティ名: 型
    count: Number
  }
}
```

もう 1 階層ネストしたオブジェクトにして `type` で型を指定し、`required`（必須かどうかの真偽値）や、`default`（省略された場合の値）などを指定するとよいでしょう。

```javascript
export default {
  props: {
    title: {         // プロパティ名
      type: String,  // 型
      required: true // 必須かどうか
    },
    count: {
      type: Number,
      default: 0     // 親から値が渡されなければ 0 になる
    }
  }
}
```
※ デフォルト値を関数で生成したり、値のバリデーション関数を定義することもできます。詳細は [Vue 公式サイト: プロパティのバリデーション](https://jp.vuejs.org/v2/guide/components-props.html#%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%81%AE%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3) を参照してください。


### setup 関数で props を使う
Composition API の `setup` 関数では、ひとつめの引数で `props` を取得できます。
親コンポーネントで値が変わったら `props` にも反映される、というリアクティブな性質をもつオブジェクトです。関数内で使用する場合は `computed` や `watch` で監視する必要があります。

```javascript:子コンポーネント
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 }
  }
}
```

ただし、引数を分割代入で取得してしまうと、**リアクティブではなくなってしまうので注意**が必要です。
（自分はたまに忘れてやってしまいます。最初はちゃんと表示されるので気づきにくいんですよね……）

```javascript:propsを分割代入
  setup ({ count }) {
    // 親コンポーネントで値が変わっても、子コンポーネントは初期値のまま変わらない
    const doubleCount = computed(() => count * 2)
```


#### TypeScript で型の情報をつける
TypeScript の場合、`props` にプリミティブな型（`String` など）を指定した場合はちゃんと型がつきますが、`Array` や `Object` を指定した場合は中身の情報がないので補完されません。

```typescript:子コンポーネント
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` という型関数が用意されていますので、それを使ってキャストすることで型の補完が効くようになります。

```typescript:子コンポーネント
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)` を実行することで、カスタムイベントを発生させることができます。

```vue:子コンポーネント
<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` は分割代入しても悪影響はありません。

```javascript
export default {
  setup (props, { emit }) {
    const handleClick = () => {
      emit('my-event')
    }

    return { handleClick }
  }
}
```

また、 `emit` には任意の数の引数を渡すことができます。

```javascript:子コンポーネント
export default {
  setup (props, { emit }) {
    const handleClick = () => {
      emit('my-event', 123, 'abc', false)
    }

    return { handleClick }
  }
}
```

## 親コンポーネントで子のイベントを受け取る方法
通常のクリックイベントなどと同じように、テンプレート内で `@` もしくは `v-on` ディレクティブを使います。

```vue:親コンポーネント
<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 つ以上の引数を指定した場合は、イベントハンドラの引数として受け取ることができます。

```javascript:子コンポーネント（前後略）
emit('my-event', 123, 'abc', false)
```
```javascript:親コンポーネント（テンプレートは前と同じ）
export default {
  components: { Child },
  setup () {
    const handleEvent = (...args) => {
      alert(args) // -> 123, 'abc', false
    }

    return { handleEvent }
  }
}
```

※ ちなみに親のテンプレートで `@my-event="handleEvent($event)"` とすると、イベントハンドラには `emit` の第 2 引数のみ（上記の場合は `123`）が入ってきます。

# 3. 双方向バインディング
さて、子に渡した親の変数を書き換えたい場合はどうしたらよいでしょう。
子コンポーネントの中で `props` の中身を直接変更しようとすると、以下のようにエラーになってしまいます。

```javascript:子コンポーネント（テンプレート略）
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 }
  }
```

そこでイベントを使います。
子コンポーネントでイベントを発生させ、その引数を（親コンポーネントの）イベントハンドラ内で代入する、という手順を踏むことで親コンポーネントの変数の値を変更できます。

```javascript:子コンポーネント
export default {
  props: {
    count: Number
  },
  setup (props, { emit }) {
    // ボタンのイベントハンドラ
    const handleClick = () => {
      emit('my-event', props.count + 1)
    }

    return { handleClick }
  }
}
```

```vue:親コンポーネント
<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 タグなどと同じ）。
先程に比べると、相当シンプルですね。

```html:親コンポーネント
<Child v-model="num" />
```

子コンポーネントでは `value` という名前でプロパティが渡されてきます。更新の際は `input` イベントを発生させます。

```javascript:子コンポーネント
export default {
  props: {
    value: Number
  },
  setup (props, { emit }) {
    // ボタンのイベントハンドラ
    const handleClick = () => {
      emit('input', props.value + 1)
    }

    return { handleClick }
  }
}
```

つまり、`v-model` は以下と同等です。

```html:親コンポーネント
<Child :value="num" @input="num = $event" />
```

※ 当記事と直接関係ないですが、 `v-model` について深く知りたい場合は、先日公開された [Vue.jsの双方向バインディング再入門](https://qiita.com/fruitriin/items/dc75af413da3661f9e78) という記事がとても参考になります。


## 3-2. `.sync`
さて、もうひとつは Vue 2.3 で加わった `.sync` 修飾子です。
親コンポーネント側はプロパティを渡す際、後ろに `.sync` を付け足します。
これも属性がひとつだけなので、すっきりしてますね。

```html:親コンポーネント
<Child :count.sync="num" />
```

子コンポーネントでのプロパティの受け取り方は通常どおりです。更新の際は `update:プロパティ名` のイベントを発生させます。

```javascript:子コンポーネント
export default {
  props: {
    count: Number
  },
  setup (props, { emit }) {
    // ボタンのイベントハンドラ
    const handleClick = () => {
      emit('update:count', props.count + 1)
    }

    return { handleClick }
  }
}
```

したがって、 `.sync` 修飾子は、以下の省略形ということになります。

```html:親コンポーネント
<Child :count="num" @update:count="num = $event" />
```

## 3-3. Vue 3.0 での変更点
来たる Vue 3.0 では、上記の `.sync` 修飾子は廃止され、 `v-model` が引数を取れるようになるそうです。

引用元: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0005-replace-v-bind-sync-with-v-model-argument.md

> Instead of:
> \<MyComponent v-bind:title.sync="title" />
>
> the syntax would be:
> \<MyComponent v-model:title="title" />

```html
<MyComponent v-model="xxx" />

<!-- would be shorthand for: -->

<MyComponent
  :model-value="xxx"
  @update:model-value="newValue => { xxx = newValue }"
/>
```
```html
<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. アンチパターン
プリミティブでない値（配列やオブジェクトなど）を渡した場合は、子コンポーネントから直接中身を変更できてしまいます。

```vue:親コンポーネント
<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>
```

```javascript:子コンポーネント
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 年前の大きな改訂](https://github.com/vuejs/jp.vuejs.org/commit/d7fed38d89d739cc50d049fecb15e1d625647497#diff-044f0befe8adfe59792a31b1763e3cc4L222) で消えていました。つい最近見た気がするけど、そんなに前だったとは……。

今回の主旨から外れますが、親子間以外でもデータを共有したい場合は [store パターン](https://jp.vuejs.org/v2/guide/state-management.html#%E3%82%B7%E3%83%B3%E3%83%97%E3%83%AB%E3%81%AA%E7%8A%B6%E6%85%8B%E7%AE%A1%E7%90%86%E3%82%92%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E4%BD%9C%E3%82%8B) を試してみたり、公式の状態管理ライブラリ [Vuex](https://vuex.vuejs.org/ja/) の導入を検討するとよいでしょう。

Vue 3.0 は Composition API 以外にも新機能がたくさんあるので待ち遠しいですね。
それでは。

# 関連記事
- [Vue.js の Composition API における this.$refs の取得方法](https://qiita.com/jay-es/items/6d0279737fb510b0aa6c)