Vue.js

Vue.js コンポーネント間の通信

こんにちは!ともです(@_tomo_engineer)!

前回はコンポーネントを定義する方法について勉強しましたが、今回はコンポーネント間でデータを受け渡しする方法について見て行きます。

参考にしたものは以下です。

基礎から学ぶVue.js(書籍)

Vue.js:日本語ドキュメント

コンポーネントの間の関係

コンポーネントの関係には『親子』または『非親子』の2つが考えられます。

  • 親子の場合
<div id="app">
   <comp-child></comp-child>
</div>

ルートインスタンスをと考えると、それに含まれたコンポーネントはだと考える事ができます。

  • 非親子の場合
<comp-friend1></comp-friend1>
<comp-friend2></comp-friend2>

また同じ階層のコンポーネントは非親子の関係にあると考える事ができます。

スコープ

親子間or非親子間において、thisでデータを参照する事ができません。

これはスコープによるものです。他の機能に影響を与えない様にコンポーネントという単位で完結しているのです。ではどの様にしてコンポーネント間でデータ通信を行うのでしょうか。

データの受け渡し

親子間のデータの受け渡し方法は3つあります。

  1. propsとカスタムイベントを使った親子間の通信
  2. イベントバスを使った非親子間の通信
  3. Vuexを使った状態管理

VuexはVue.jsの状態管理ライブラリで、規模が大きくなった場合の管理を手助けしてくれるものです。今回はVuexは省略し、親子間と非親子間の通信について見て行きます。

上図の流れで親子間のデータを受け渡しします。

親→子にデータを渡す場合は、属性で渡し、propsで受け取る

子→親にデータを渡す場合は、$emitで渡し、onで受け取る

親から子へはデータを橋渡しするだけで良いですが、子から親へはイベントを発生させて受け渡しを行う必要があります。それぞれについて見て行きます。

親→子

親から子へは、属性とpropsがキーワードでした。

次の様に親から子へデータを渡します。

親が子に渡す(属性で渡す)

<div id="app">
        <my-component val="Aへのデータ"></my-component>
        <my-component val="Bへのデータ"></my-component>
</div>

親から子へは属性で渡します。valという属性を定義し上記の様に格納します。この属性名は任意です。

子が受け取る(props)

子は親からのデータをpropsというオプションを介して受け取ります。子のコンポーネントを次の様に定義しました。

var myComponent = {
    template: '<p>{{val}}</p>',
    props: ['val'],
}

propsオプションに親から子へデータを渡す際の属性名(今回の場合はval)と同じものを指定しています(props:[‘val’])。

あとはデータと同様に{{val}}の様にして使用できます。描画結果としては次の様になります。

<p>Aへのデータ</p>
<p>Bへのデータ</p>

親のデータを子へ与える(v-bind)

上記の場合はテキストを親から子に渡しただけでした。親コンポーネントがもつデータを子コンポーネントに渡す場合はデータバインディングを利用します。

親のインスタンスにparentDataというデータを持たせます。

new Vue({
    el: '#app',
    data: {
        parentData: 'parentDataです'
    },
    components: {
        'my-component' : myComponent
    }
})

子コンポーネントでは次の様にデータを渡します。

<div id="app">
        <my-component val="Aへのデータ"></my-component>
        <my-component v-bind:val="parentData"></my-component>
</div>

1つ目の子要素にはテキストを与え、2つ目の子要素にはv-bindを利用し親のデータ(parentData)を与えました。

<p>Aへのデータ</p>
<p>parentDataです</p>

親から子へ配列・オブジェクトを与える

<div id="app">
        <my-component val="[1,2,3]"></my-component>//テキストを与える
        <my-component v-bind:val="[1,2,3]"></my-component>//配列を与える
        <my-component v-bind:val="{name: 'hoge'}"></my-component>//オブジェクトを与える
</div>

親から子に配列orオブジェクトを渡す場合はv-bindで渡す必要があります。

上記3つの親から子へ渡したデータは全て静的なデータではありますが、配列・オブジェクトを与える際はv-bindで渡す必要があります

以上が親か子へのデータの受け渡しです。子から親へのデータの受け渡しを見る前にpropsに関する注意点があります。

propsで受け取ったデータは書き換えてはいけない

propsで受け取ったデータは親側の更新と同期しています。

親から借りてきたデータですので、子コンポーネント側で勝手に書き換えてはいけません。書き換わりはしますがエラーを吐きます。

vue.esm.js:592 [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop’s value. Prop being mutated: “val”

書き換えた値を表示したいのであれば算術プロパティを使うなり、一度子コンポーネントのデータに格納してから加工するなりする必要があります。

propsで受け取るデータの型を指定する

var myComponent = {
    template: '<p>{{val}}</p>',
    props: ['val'],
}

の様にpropsを定義した場合は型は指定されていません。

しかし次の様に型を指定して受け取る事ができます。型の指定は推奨されているので、型は指定しましょう。

var myComponent = {
    template: '<p>{{val}}</p>',
    props: {
        val: String // 文字列型のみ許可する
    }
}

この様に書けば文字列のみが許可され、数値を受け取った場合は警告で知らせてくれます。これは問題の箇所の発見に役立ちます。

[Vue warn]: Invalid prop: type check failed for prop “val”. Expected Number, got String.

propsでvalidationを作成する

propsが受け取るデータはしっかり定義すると良いです。

下記のコードは日本語ドキュメントから拝借しました。

// デフォルト値つきの数値型
    propD: {
      type: Number,
      default: 100
    },
    // デフォルト値つきのオブジェクト型
    propE: {
      type: Object,
      // オブジェクトもしくは配列のデフォルト値は
      // 必ずそれを生み出すための関数を返す必要があります。
      default: function () {
        return { message: 'hello' }
      }
    },
    // カスタマイズしたバリデーション関数
    propF: {
      validator: function (value) {
        // プロパティの値は、必ずいずれかの文字列でなければならない
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
}

注目すべきはオブジェクトをデフォルトとする場合は、オブジェクトを返す関数とする必要があるという事です。

また、type(型の指定)・required(必須データの指定)で不足の場合は、カスタムバリデータも準備されています。

カスタムバリデータは条件を満たすかの真偽を返す関数として定義します。至れり尽くせりです。

子→親

子から親へは、$emitとonがキーワードでした。

次の様に親から子へデータを渡します。

event up

  1. 親子間のイベントの動線を貼る
  2. 子が$emitでイベント発火を親に伝える
  3. 親は子からのイベント発火を受け取り親メソッドを起動する

この流れで子から親にイベントを伝える事(event up)ができます。

この3つについて見て行きます。

①親子間のイベントの動線を貼る

<div id="app">
   // child-eventが発火したら、parentMethodを発火して下さい
        <my-component v-on:child-event="parentMethod"></my-component>
</div>

この様に子のイベント発火をフックに、親のメソッドを起動させる事ができます。

②子が$emitでイベント発火を親に伝える

emitは『発する』という意味を持ち、イベントの発火を意味しています。

子コンポーネントを次の様に定義しました。

var myComponent = {
    template: '<p><button v-on:click="click">子から親にイベント発火</button></p>',
    props: {
        val: Array
    },
    methods: {
        click: function(){//ボタンをクリックするとこのメソッドが起動する
            this.$emit('child-event');//親へイベント発火を伝える
        }
    }
}

ボタンをクリックするとthis.$emit('フック名')で親にイベント発火を伝えます。今回の場合、フック名はchild-eventです。

③親は子からのイベント発火を受け取り親メソッドを起動する

this.$emit('child-event')で子から親にイベントの発火を通知しv-on:child-event="parentsMethod"で親はその通知を受け取りました。

これにより親のparentsMethodが起動します。次の様に親のインスタンスを定義しました。

new Vue({
    el: '#app',
    components: {
        'my-component' : myComponent
    },
    methods: {
        parentMethod: function(){
            alert('親がイベントを受け取った');
        }
    }
})

parentMethodが起動し、上記の様なalertを表示してみます。

子から親にデータを渡す

event upのフローを確認しました。

本題の子から親にデータを渡す方法ですが、even upのフローの$emitの第二引数で子から親にデータを渡します。

var myComponent = {
    template: '<p><button v-on:click="click">子から親にイベント発火</button></p>',
    props: {
        val: Array
    },
    data: function(){
        return{
            message: 'Hello Vue.js'
        }
    },
    methods: {
        click: function(){
            // 第二引数で子のデータを格納
            this.$emit('child-event', this.message);
        }
    }
}
new Vue({
    el: '#app',
    components: {
        'my-component' : myComponent
    },
    methods: {
        parentMethod: function(fromChildVal){
    // $emitの第二引数のデータが入っている
            alert(fromChildVal);
        }
    }
})

$emitの第二引数に子から親に渡したいデータをセットし、親のメソッドで受け取る事ができます。event upのフローさえ覚えていれば単純でした。

非親子間のデータ受け渡し

非親子コンポーネント間では、propsを使ってデータ通信できません。その代わり『イベントバス』を利用して通信を行います。

イベントバス』とはVueインスタンを経由して非親子のコンポーネント間で通信を行う方法です。

上図の様なイメージです。コンポーネント1からコンポーネント2へイベント通知します。その際にイベントバスを経由するという構図です。

次の様に2つのコンポーネントを定義しました。

#index.html
<div id="app">
        <my-component1></my-component1>
        <my-component2></my-component2>
</div>
#main.js
import Vue from 'vue';

// 『イベントバス』のためのインスタンス
var bus = new Vue();

// myComponent1→myComponent2にイベントを通知する($emit)
var myComponent1 = {
    template: '<p><button v-on:click="click">子から子へイベントバスで通知</button></p>',
    props: {
        val: Array,
    },
    data: function(){
        return{
            message: 'I am emit'
        }
    },
    methods: {
        click: function(){
            bus.$emit('bus-event', this.message);
        }
    }
}

// myComponet1からのイベントを受け取る($on)
var myComponent2 = {
    template: '<p>コンポーネントBでbusのデータを表示:{{ message }}</p>',
    data: function(){
        return {
            message: '元のデータ',
        }
    },
    mounted: function(){
        bus.$on('bus-event', this.changeMethod)
    },
    methods: {
        changeMethod: function(message){
            console.log(message);
            this.message = message;
        }
    }
}

new Vue({
    el: '#app',
    components: {
        'my-component1' : myComponent1,
        'my-component2' : myComponent2,
    },
})

myComponent1とmyComponet2を通信を行うために、イベントバス用のVueインスタンスvar bus = new Vue();を作成しました。イベントバス用のVueインスタンスの$emitや$onを利用してコンポーネント間で通信を行います。

機能としては、myComponent1のボタンクリックをフック($emit)に、myComponent1のデータをmyComponent2に送信しています。$emitの第二引数に送信したいデータをセットします。

methods: {
        click: function(){
            bus.$emit('bus-event', this.message);
        }
}

myComponent2のマウント時に、bus.$onにより非親子間のイベントの登録しておきます。bus-eventが発火したタイミングでchangeMethodメソッドを起動する様に設定しています。

mounted: function(){
        bus.$on('bus-event', this.changeMethod)
},

changeMethodメソッドは次の様に作成しました。引数には$emitの第二引数にセットしたデータが入っています。

methods: {
        changeMethod: function(message){
            this.message = message;
        }
}

これでComponent1からComponent2にデータ(今回の場合はmessage)が渡りました。

まとめ

今回はVue.jsのコンポーネント間の通信について進めました。

親→子 or 子→親 or 非親子関係かで通信方法は違いました。まとめると以下の様になります。

まとめ
  • 親→子の場合は属性で渡し、propsで受け取る
  • 子→親の場合は、子で$emitでイベントの発火し、親でイベントを検知する(event up)
  • 非親子間の場合は、イベントハブを経由し、$emitで発火、$onで発火を検知する

頭を整理するのに苦労しましたが、まとめて見ました。この様にデータの管理は規模が大きくなるにつれて辛くなってくる事がわかります。それを解決するVuexというライブラリもあります。まずはVue.jsでのデータ通信の基礎を固めて次に進んで行きたいです。