見出し画像

Vue.js基礎~コンポーネント間のデータの受け渡し~

皆さんこんにちは。PoSoです。
だんだんと更新速度が落ちている気がしますが、気のせいではないです。

結局自らが定めた決め事を1月足らずしか継続できずに半ばグレておりました。
しかし、いつも失敗で終わらせてきたから変わらないのだと誰かが言っていたのを耳にして、以前の目標を数段レベルダウンさせて再スタートすることとしました。
最近は近しい友人がプログラミング学習を始め、これから切磋琢磨して学習を続けて行けそうです。

さて、今回はコンポーネント間のデータの受け渡しについてです。
実際によく使う機能なのでぜひ最後までお付き合いいただければと思います。

なぜデータの受け渡しが必要なのか

まずはなぜ親子間のコンポーネントでデータの受け渡しが必要なのかを理解していきましょう。

そもそも「親コンポーネント」「子コンポーネント」とはどれを指すのかについて説明します。

画像1

App.vue

<template>
 <div>
   <LikeHeader></LikeHeader>
   <LikeNumber></LikeNumber>
 </div>
</template>

<script>
  ...
</script>

<style scoped>
  ...
</style>

例えばこのような構造でコードが書かれていたとしたら、App.vueから見て、LikeHeaderやLikeNumberは「子コンポーネント」になります。
逆にLikeHeaderやLikeNumberから見たら、App.vueは「親コンポーネント」となります。

コンポーネントを登録して使っているのが「親」、使われているのが「子」と理解できます。


親子間のデータの受け渡しの必要性を説明するのにブログを例にしていこうと思います。

Vue.jsでブログが構築されているとして想像してみてほしいのですが、その構造はどうなっているでしょうか。
タイトルがあって…内容があって…いいね数みたいなものがあったりして…

これらは別々のコンポーネントを使用して作っていくことになりますよね。

そしてブログのタイトルや内容は、開発中に書けるような静的な内容ではないです。
どこかのサーバーからAPIをたたいたりして、データベースを取ってきて表示させるはずです。

しかし、いざデータを表示させようとしても、中身が細かくコンポーネントで分かれています。
再利用可能なVueインスタンスを作る、それがコンポーネントの概念のミソなのですから、当然です。

ですから、このデータはタイトル…このデータは内容…と、細かくデータを振り分ける必要があると想像できますでしょうか。

このように、ブログ1つを例に取っても、1つのコンポーネントから細部のコンポーネントにデータを渡したいというシーンはたくさんあります。
コンポーネントを使用してアプリケーションを作っていく際には理解しなくてはいけない部分であるとわかってもらえたと思います。

次項からは具体的にどうやってデータを渡していくのかについて書いていきますが、やり方が2種類あります。
正確には「親から子」と「子から親」でやり方が違います。

ここを理解したうえで見ていただけるとわかりやすいと思います。


親から子へのデータの受け渡し「props」

・使用方法

親から子へのデータの受け渡しには「props」というオプションを使用します。
子コンポーネントでこれを定義します。

LikeNumber.vue

<template>
 <div>
   <p>いいね({{ number / 2 }})</p>
   <button @click="increment">+1</button>
 </div>
</template>

<script>
 export default {
   props: ['number'],
   methods: {
     increment() {
       this.number += 1
     },
   },
 }
</script>

propsは配列で、括弧の中にはプロパティの名前を書きます。
名前は何でもOKです。

次に親から子に渡してあげないといけないので、渡す値を決めます。

App.vue

<template>
 <div>
   <LikeHeader></LikeHeader>
   <h2>{{ number }}</h2>
   <LikeNumber number="6"></LikeNumber>
   <LikeNumber number="6"></LikeNumber>
 </div>
</template>

<script>
 import LikeHeader from './components/LikeHeader.vue'

 export default {
   data() {
     return {
       number: 10,
     }
   },
   components: {
     LikeHeader,
   },
 }
</script>

propsは親ではHTMLの属性のように定義でき、そこに実際に値を入れていきます。

v-vindを使用して動的に扱うことも可能です。

<template>
 <div>
   <LikeHeader></LikeHeader>
   <h2>{{ number }}</h2>
   <LikeNumber :number="number"></LikeNumber>
   <LikeNumber :number="number"></LikeNumber>
 </div>
</template>

<script>
 import LikeHeader from './components/LikeHeader.vue'

 export default {
   data() {
     return {
       number: 10,
     }
   },
   components: {
     LikeHeader,
   },
 }
</script>

これでApp.vueのデータがLikeMumber.vueで使用できている状態ができました。
実際にブラウザで確認してみたりnumberの値を変えてみたりして動きを確認してみるといいと思います。


・命名規則

propsの命名方法にはキャメルケースとパスカルケースがあります。

LikeNumber.vue

<p>いいね({{ totalNumber / 2 }})</p>

...

props: ['totalNumber']

App.vue

<LikeNumber :totalNumber="number"></LikeNumber>
<LikeNumber :total-number="number"></LikeNumber>

JavaScriptではキャメルケースですが、templateではキャメルケースでもケバブケースでも動作します。
理由は以前の記事でも説明していますが、DOMテンプレートを使用する際にケバブケースを使用するからです。

それではどちらを使用するのがいいのか、となりますが、公式に記載があります。
・JavaScript内ではキャメルケースを使用
・テンプレート内ではケバブケースを使用
となります。

HTMLの慣習として属性名はケバブケースを使用するのが一般的ですし、公式でも明言されているのでこのようにするのがいいと思います。


・コンポーネント内でのpropsの扱い方

LikeNumber.vue

<p>いいね({{ halfNumber }})</p>

...

computed: {
     halfNumber() {
       return this.totalNumber / 2
     }
   },

基本的にdataと同じ要領でthisを使用してアクセスすることが可能です。


・バリデーションの使用

バリデーションとは値が正確かどうかを確かめる処理のことを言います。
分かりにくい方はこちらを参照ください。

LikeNumber.vue

props: {
 totalNumber: Number
},

いままで配列で指定してきたpropsですが、このようにオブジェクトで指定することもできます。
keyに名前、valueに型の種類を記載します。

例としてvalueをStringに変更すると、開発モードのブラウザのコンソールに警告が出ます。

画像2

大規模な開発で、様々なデータのやり取りがコンポーネント間である場合に、原因不明のバグやエラーを防いだり、早期発見できるようにしたりすることに有効です。

さらに型だけではなく様々なものを複数指定できます。

LikeNumber.vue

props: {
 totalNumber: {
       type: Number,
       required: true,
 }
}

App.vue

<LikeNumber :total-number="number"></LikeNumber>
<LikeNumber></LikeNumber>

このようにtotalNumberもオブジェクトで指定することができます。

required: trueは「必ずこの属性は必要か否か」を決めるものです。

上記のようにLikeNumberに何も属性が指定されていないと警告が表示されます。

画像3


このように必要に応じてバリデーションを使用しながら開発を行っていくと、バグやエラーの対処にかかるコストが軽減できます。
ぜひ利用していきましょう。


・複数のpropsを付ける方法

LikeNumber.vue

props: {
  totalNumber: {
    type: Number,
    required: true,
  },
  tesProps: {
    type: String
  }
}
props: ['totalNumber', 'testProps']

上がオブジェクトの場合、下が配列の場合です。
説明が要らないほど単純です。


子から親へのデータの受け渡し「$emit」

・使用方法

次は子から親にデータを渡す方法ついて解説していきます。

子から親の場合は「$emit」を使用します。
これはイベントハンドラのようなもので、カスタムイベントを生成し、そこにデータを紐付けて処理をしていく感じになります。

今回はボタンが押されたときにデータが渡るような処理にしていこうと思います。

LikeMumber.vue

<template>
 <div>
   <p>いいね({{ halfNumber }})</p>
   <button @click="increment">+1</button>
 </div>
</template>


<script>
 export default {
   props: ['totalNumber'],
   computed: {
     halfNumber() {
       return this.totalNumber / 2
     },
   },
   methods: {
     increment() {
       this.$emit('my-click', this.totalNumber + 1)
     },
   },
 }
</script>

今回は練習なので古いコードを消してincrementメソッドにそのまま書いてしまいます。

this.$emit()で接続できます。
中には2つ引数を取ります。
第一引数がイベントの名前、第二引数が渡すデータを書きます。

this.totalNumberでpropsに接続し、+1して返します。
これでデータの「送り口」の部分は完成しました。

続いて「受け口」の方を作っていきます。

App.vue

<template>
 <div>
   <LikeHeader></LikeHeader>
   <h2>{{ number }}</h2>
   <LikeNumber :totalNumber="number" @my-click="number = $event"></LikeNumber>
   <LikeNumber></LikeNumber>
 </div>
</template>

LikeNumberコンポーネントのタグの中に書いていきます。

v-onディレクティブで先ほど付けたイベントの名前を定義します。
「””」の中にはJavaScriptが書けますので、ここで$eventで先ほどの+1した値にアクセスし、App.vueのnamberと=で結ぶ式を書きます。

つまり、クリックイベントが発火した瞬間にnamber(14)+1の15が置き換わるということになります。

ここで$emitについて本質的な役割を解説します。
$emitは子から親にデータを渡すもの、というよりかは、子コンポーネントの中で親コンポーネントのメソッドを、好きなタイミングで発火できるものと表現できます。

別にclickイベントでなくてもよく、createdやaddなどいろいろなところに$emitは置くことができます。
さらには第二引数も無くても機能します。
付随してデータを送ることができる、というだけです。

今までv-onディレクティブで指定できるイベントは決まっていました。
しかし今回好きな名前で作ったイベントを指定できています。
つまり$emitとは「カスタムイベント」を作るものなのです。

まとめます。
・子から親にデータを渡す場合は「$emit」を使用する
・「$emit」は特定のコンポーネントにおける特定のタイミングで発火する
 カスタムイベントを生成し、それに乗じてデータを渡している


・なぜ$emitはpropsと違って複雑なのか

ひとえに子に依存した親のデータというものが存在しないからです。

皆さんここで考えてみてほしいのですが、親から子のデータが書き換えれることに加え、子から親のデータを書き換えることが可能になった場合、そのデータフローはかなり複雑になると思いませんか?
親から子の単方向に限定したフローの方が分かりやすいし、したがってメンテナンス性もよいです。
このような理由から、Vue.jsではこの単方向のデータフローを意図的に行っています。

よって、$emitで行っているデータの変更は実際は親コンポーネントが行っています
その証として、App.vueのデータの受け口では「number = $event」と処理を書いています。
$emitで書いた処理というのは、ただ「totalNumberが+1された値」を生成したにすぎません。

親から子のやり方と、子から親のやり方が全然違うからわかりにくいなぁ…と思うかもしれませんが、逆にこの制限があることによって、全体を見た時にわかりやすくなっているんだな、と思っていただければと思います。


・命名規則

$emitで付けた第一引数の名前の書き方についてですが、ケバブケースで統一するのがよいとされています。

理由は、カスタムイベントに関してはJavaScriptで使用する機会が皆無だからです。
$emitの中では第一引数は文字列として定義していて、それ以上JavaScriptで使用する機会はなく、唯一templateタグの中で使用します。

ならばケバブケースで統一した方がよいでしょうとなったわけですね。


最後まで読んでいただいてありがとうございました。
つたない文章ですが僕のような初学者にも伝わるような文を心掛けています。

こんな感じで最低2週に1記事くらいのペースで、現在学習しているVue.jsについての記事を書いています。
もし、ためになった、今後も記事を読んでみたいと思っていただけたら、ハートとフォローの方をよろしくお願いいたします。

次回はslotについて書いていきます。

この記事が気に入ったらサポートをしてみませんか?