Hanatare's PaPa

Make life a little richer.

Virtual Space of Hanatare's PaPa

人生をほんの少しだけ充実させる

【Vue.js 3】親子間のコンポーネントの通信

Vue.jsでアプリケーションを開発していると、「コンポーネント」という部品をたくさん作ることになります。このコンポーネント、単体で完結することは少なく、他のコンポーネントと連携して初めて意味のある機能を実現します。その連携の要となるのが、「コンポーネント間のデータ受け渡し」です。

Vue.jsには、このデータ受け渡しの方法がいくつか用意されており、状況に応じて最適な方法を選ぶことが、見通しが良くメンテナンスしやすいアプリケーションを作る鍵となります。

今回は、最も基本的で重要な「親子間の通信」を担う「props」と「emit」について記事にしたいと思います。前提としてこの記事では、Vue3での記法を前提とします。

記事のポイント
  • 親から子へ:propsを使ったデータ受け渡し
  • 子から親へ:emitを使ったイベント通知

propsによるデータの受け渡し:親から子への一方通行

「props(プロップス)」は、親コンポーネントから子コンポーネントへデータを渡すための最も基本的な仕組みです 。データは親から子への一方通行で流れるのが特徴で、これを「One-Way Data Flow(一方通行のデータフロー)」と呼びます 。この原則のおかげで、データの流れが予測しやすくなり、アプリケーションの管理が容易になります。

definePropsの基本的な使い方

「script setup」構文では、definePropsというコンパイラマクロを使って子コンポーネントが受け取るpropsを宣言します 。コンパイラマクロとは、事前に定義された処理となるので、refのようにimportが必要になりません。

配列構文

最もシンプルな方法は、受け取るpropsの名前を文字列の配列で指定するものです 。

子コンポーネント (ChildComponent.vue)

<script setup>
// 'message'と'user'という名前のpropsを受け取ることを宣言
const props = defineProps(['message', 'user'])

console.log(props.message) // 親から渡されたメッセージが表示される
</script>

<template>
  <div>{{ message }}</div>
  <p>ようこそ、{{ user }}さん</p>
</template>

親コンポーネント (ParentComponent.vue)

<script setup>
import ChildComponent from './components/ChildComponent.vue'
</script>

<template>
  <ChildComponent message="こんにちは、Vueの世界へ!" user="ハナタレ" />
</template>

上記のサンプルでは、親コンポーネントから、親コンポーネントと同じ階層にあるcomponentsディレクトリにある、ChildComponent.vueを呼び出しています。その際、templateないで、messageとuser変数を使い、子コンポーネントに値を渡しています。この方法は手軽で、小規模なコンポーネントやプロトタイピング向きと言えますが、型チェックや必須チェックができないため、より堅牢なコンポーネントを作る場合は、オブジェクト構文での宣言が推奨されています。

オブジェクト構文

より堅牢なコンポーネントを作るためには、オブジェクト構文での宣言が推奨されます 。各propsに対して、期待する型(String, Number, Booleanなど)や必須有無(required)、デフォルト値(default)、カスタムバリデーション関数(validator)を指定できます。

  • type: String, Number, Boolean, Arrayなど期待する型を指定でき、配列を用いることで複数の型を指定することも可能です
  • required: trueにすると、そのpropが必須になります。
  • default: propが渡されなかった場合のデフォルト値を設定します。
  • validator: カスタムのバリデーション関数を定義できます。
<script setup>
defineProps({
  // 型チェックのみ
  propA: Number,

  // 必須の文字列
  propB: {
    type: String,
    required: true
  },

  // デフォルト値を持つ数値
  propC: {
    type: Number,
    default: 100
  },

  // カスタムバリデーション関数
  propD: {
    type: String,
    validator: (value) => {
      // 値は'success', 'warning', 'danger'のいずれかでなければならない
      return ['success', 'warning', 'danger'].includes(value);
    }
  }
});</script>

一方通行のデータフロー(One-Way Data Flow)の重要性

ここで改めて「一方通行のデータフロー」の重要性に触れておきます。子コンポーネントは、親から受け取ったpropsを直接変更してはいけません 。もし子が好き勝手にpropsを変更できてしまうと、データの変更がどこで起きたのか追跡するのが非常に困難になり、アプリケーションはあっという間に複雑で手に負えないものになってしまいます。

親コンポーネントがデータの「唯一の真実の源(Single Source of Truth)」であり続けることが、予測可能なアプリケーションを構築する上で不可欠なのです。

では、子コンポーネント内でpropsの値を元に何か処理をしたい場合はどうすればよいのでしょうか?正しいパターンは2つあります 。

ローカルのリアクティブなデータとして初期化する

propsの値を初期値として、子コンポーネント内に新しいrefを定義します。

<script setup>
import { ref } from 'vue';

const props = defineProps(['initialCounter']);

// props.initialCounterは初期値としてのみ使用される
const counter = ref(props.initialCounter);

// このcounterは子コンポーネント内で自由に更新できる
function increment() {
  counter.value++;
}
</script>

上記のように、親から受け取った値をcounterという子コンポーネントないで定義したref値に再代入することで、子コンポーネント内で値を自由に操作することが可能になります。

computedプロパティを使う

propsの値を元に加工した値を使いたい場合は、computedプロパティが最適です。親のpropsが更新されると、computedプロパティも自動的に再計算されます。computedはリアクティブシステムを保ったまま、式を変数に格納することできます。その際、computed内部で使用されたリアクティブオブジェクトを処理実行後も監視をし続けるために、computed内で監視されている値に変更が起こると都度関数を実行して結果を再描画します。

<script setup>
<script setup>
// propsで値を受け取ることを宣言
import { computed } from 'vue'

const props = defineProps(['size'])

// props.sizeが変更されると自動的に更新される算出プロパティ
const normalizedSize = computed(() => props.size)
</script>

<template>
  <div>{{ normalizedSize }}</div>
</template>

emitによるイベント発行:子から親への通知

「props」が親から子へのデータ伝達だったのに対し、子から親へ何かを伝えたい場合は「emit」を使います。「emit」は、子コンポーネントが「イベント」を発行(emit)し、親コンポーネントに通知するための仕組みです。親は、そのイベントを購読(listen)して、特定の処理を実行できます 。

defineEmitsによるイベントの宣言

「props」と同様に、「script setup」ではdefineEmitsマクロを使って、そのコンポーネントが発行する可能性のあるイベントを宣言します 。

<script setup>
// 'update'と'delete'という名前のイベントを発行することを宣言
const emit = defineEmits(['update', 'delete']);

function onButtonClick() {
  // 'update'イベントを発行する
  emit('update');
}
</script>

defineEmitsはemit関数を返します。この関数を使って、「script setup」内からイベントを発行します。

イベントの引数を親に渡す

イベントを発行する際に、親にデータを渡したい場合があります。これを「ペイロード」と呼びます。emit関数の第2引数以降に値を含めることで、ペイロードを渡すことができます 。

子コンポーネント

<script setup>
defineEmits(['reset'])
</script>
<template>
  <button @click="$emit('reset', '古いハナタレ')">button</button>
</template>

または、

<script setup>
const emit = defineEmits(['reset'])
function emitRes() {
  emit('reset', '古いハナタレ')
}
</script>
<template>
  <button @click="emitRes">button</button>
</template>

親コンポーネント

親コンポーネントは、v-onディレクティブ(@という省略形が一般的)を使って子コンポーネントのイベントをサブスクリプションします 。

<script setup>
import { ref } from 'vue'
import ChildComponent from './components/ChildComponent.vue'

const name = ref('ハナタレ')

// イベントハンドラメソッド。ペイロードは第一引数として渡される
function updateName(newName) {
  name.value = newName
}
</script>

<template>
  <p>現在の名前: {{ name }}</p>
  <ChildComponent @reset="updateName" />
</template>

子コンポーネントが$emit('reset', '古いハナタレ')を実行すると、親コンポーネントの@resetメソッドが呼び出され、引数のupdateNameに「古いハナタレ」が渡されます。

まとめ

今回は、Vue.jsにおけるコンポーネント間通信の最も基本的な形であるpropsとemitについてまとめました。

  • props: 親から子へデータを渡すための一方通行の仕組み。
  • emit: 子から親へイベントとデータを通知するための仕組み。

この「propsで下ろし、emitで上げる」というパターンはVueアプリケーションの基本構造を形作ります。まずはこの形をしっかりと理解していければと思います。今回の記事が今回の記事がVue.jsを使う方の参考になれば幸いです。