136
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

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://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 にも反映される、というリアクティブな性質をもつオブジェクトです。関数内で使用する場合は computedwatch で監視する必要があります。

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

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

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

TypeScript で型の情報をつける

TypeScript の場合、props にプリミティブな型(String など)を指定した場合はちゃんと型がつきますが、ArrayObject を指定した場合は中身の情報がないので補完されません。

子コンポーネント
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 が引数を取れるようになるそうです。

引用元: 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" />

<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 という言葉で説明されていました。1 このページで説明してきた内容を端的に表している言葉ですね。

今回の主旨から外れますが、親子間以外でもデータを共有したい場合は store パターン を試してみたり、公式の状態管理ライブラリ Vuex の導入を検討するとよいでしょう。

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

関連記事


  1. GitHub をさかのぼってみたら、2 年前の大きな改訂 で消えていました。つい最近見た気がするけど、そんなに前だったとは……。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
136
Help us understand the problem. What are the problem?