はじめに
Vue.js とは JavaScript を元にしたフレームワークであり、単一ファイルコンポーネントを利用します。コンポーネントを組み合わせて使うのでコンポーネント間でデータを受け渡すのが重要であり、それについて既に多くの方が記事に起こしています。
数多くの技術記事がありますが、
- Vue.js のバージョンが上がった
- Composition API での記述は少ない
のが気がかりです。
よって今回は、現時点での Composition API におけるコンポーネント間データ連携のやり方をまとめようと思います。
Composition API ?
Vue.js には Composition API と Options API があります。それぞれの違いやメリット・デメリットの詳細は以下のドキュメントを参照していただきたいのですが、簡単にいうと...
-
Composition API →
<script setup>
を使う -
Options API →
<script>
を使う
という使い分けができます。
コンポーネント間データ連携のまとめ
コンポーネントを取り込む側を親コンポーネント、取り込まれる側を子コンポーネントといいます。以下のような vue ファイルを記述することで、コンポーネントを利用できます。
<script setup>
// 利用したいコンポーネントをインポートします
import ChildComponent from "@/components/ChildComponent.vue";
</script>
<template>
<div>
<h3>親コンポーネント</h3>
<div class="parent-container">
<!-- ここにコンポーネントを配置します -->
<ChildComponent />
</div>
</div>
</template>
<style scoped>
.parent-container {
padding: 50px;
}
</style>
<script setup>
</script>
<template>
<div class="child-container">
<h3>子コンポーネント</h3>
</div>
</template>
<style scoped>
.child-container {
height: 20vh;
border: 5px solid black;
}
</style>
CSSでスタイルを適用しているので、画面は以下のようになります。
↓↓↓
以下に各種バージョンを記載します。
- vue@3.5.13
- vue-router@4.5.1
- vite@6.3.3
- node:v22.5.1
- npm:10.8.2
API まとめ
今回は4種類のシチュエーションを想定して、それぞれに対して最適な API を紹介します。
シチュエーション | API |
---|---|
親で定義した変数を子で使いたい | defineProps |
子で使うイベントを親で定義したい | defineEmits |
親と子で同じ変数を扱いたい | defineModel |
子で定義したメソッドを親で利用したい | defineExpose |
なお、これらは以下の公式ドキュメントから引用していますので、併せてご参照ください。
親で定義した変数を子で使いたい
defineProps を使いましょう
<script setup>
import ChildComponent from "@/components/ChildComponent.vue";
import { ref } from "vue";
// 渡すデータをここで定義します
const title = ref("タイトル");
const detail = ref("詳細");
</script>
<template>
<div>
<h3>親コンポーネント</h3>
<p>定義した変数→ {{title}}</p>
<p>定義した変数→ {{detail}}</p>
<div class="parent-container">
<!-- propsで変数を渡します -->
<ChildComponent
:title="title"
:detail="detail"
/>
</div>
</div>
</template>
<script setup>
// definePropsを用いて API を定義します
const props = defineProps({
title: {
type: String
},
detail: {
type: String
},
});
</script>
<template>
<div class="child-container">
<h3>子コンポーネント</h3>
<!-- 親から受け取った変数を利用します -->
<p>"title"で渡されたデータ→ {{title}}</p>
<p>"detail"で渡されたデータ→ {{detail}}</p>
</div>
</template>
↓↓↓
子コンポーネントで defineProps
を利用して API を定義することができます。上コードのように<template>
で props 内の変数を使うことができます。
<script setup>
内で使うときは props.
をつけるようにします。
<script setup>
const props = defineProps({
title: {
type: String
},
detail: {
type: String
},
});
// ReferenceError: title is not defined のエラーが出ます
console.log(title);
// 正しく出力されます
console.log(props.title);
</script>
defineProps の戻り値からの分割代入でも変数を利用できます。
<script setup>
// ここで分割代入
const { title, detail } = defineProps({
title: {
type: String
},
detail: {
type: String
},
});
// 正しく出力されます
console.log(title);
console.log(detail);
</script>
props で渡すデータは読み取り専用です。
よって「データは1回渡せればよく、動的に変更する予定がない」場合は defineProps
を利用すれば良いでしょう。
defineProps では詳細な定義を行うことを推奨します。
https://ja.vuejs.org/style-guide/rules-essential.html
子で使うイベントを親で定義したい
defineEmits を使いましょう
<script setup>
import ChildComponent from "@/components/ChildComponent.vue";
import { ref } from "vue";
const count = ref(0);
// メソッドを定義します
const handleCounter = () => {
count.value++;
};
</script>
<template>
<div>
<h3>親コンポーネント</h3>
<p>押した回数→ {{count}}</p>
<div class="parent-container">
<!-- 定義したメソッドをここで指定します -->
<ChildComponent
@counter="handleCounter"
/>
</div>
</div>
</template>
<script setup>
// ここで emits を定義します
const emit = defineEmits(["counter"]);
</script>
<template>
<div class="child-container">
<h3>子コンポーネント</h3>
<!-- emit でメソッドを実行します -->
<button @click="emit('counter')">ボタン</button>
</div>
</template>
↓↓↓
子コンポーネントで defineEmits
を利用することで、「ボタン」をクリックしたときに何をするかを親コンポーネントで定義できます。
引数もつけることができます。
<script setup>
import ChildComponent from "@/components/ChildComponent.vue";
import { ref } from "vue";
const count = ref(0);
// クリックしたら num の数だけ count が増えていきます
const handleCounter = (num) => {
count.value += num;
};
</script>
<template>
<div>
<h3>親コンポーネント</h3>
<p>押した回数→ {{count}}</p>
<div class="parent-container">
<ChildComponent
@counter="handleCounter"
/>
</div>
</div>
</template>
<script setup>
const emit = defineEmits(["counter"]);
</script>
<template>
<div class="child-container">
<h3>子コンポーネント</h3>
<!-- 引数をつけています -->
<button @click="emit('counter', 2)">ボタン</button>
</div>
</template>
↓↓↓
defineEmits
は子コンポーネントのイベントを親で定義できます。
「子コンポーネントの HTML は汎用的に利用したいが、イベントは親ごとに異なる」場合に利用すると良いでしょう。
親と子で同じ変数を扱いたい
defineModel を使いましょう
<script setup>
import ChildComponent from "@/components/ChildComponent.vue";
import { ref } from "vue";
// ここで入力内容を格納する変数を定義します
const text = ref("初期値");
</script>
<template>
<div>
<h3>親コンポーネント</h3>
<p>入力内容→ {{text}}</p>
<div class="parent-container">
<!-- ここで変数を指定します -->
<ChildComponent
v-model="text"
/>
</div>
</div>
</template>
<script setup>
// ここで defineModel を定義します
const text = defineModel();
</script>
<template>
<div class="child-container">
<h3>子コンポーネント</h3>
<!-- ここで v-model を指定します -->
<input type="text" v-model="text" />
</div>
</template>
↓↓↓
バージョン 3.4 以降、子コンポーネントで defineModel
を利用することで、親と子の双方向で同じ変数を動的に扱うことができます。
defineModel
の引数に文字列を指定することで複数の双方向バインディングを実装することができます。
<script setup>
import ChildComponent from "@/components/ChildComponent.vue";
import { ref } from "vue";
const title = ref("title初期値");
const detail = ref("detail初期値");
</script>
<template>
<div>
<h3>親コンポーネント</h3>
<p>title の入力内容→ {{title}}</p>
<p>detail の入力内容→ {{detail}}</p>
<div class="parent-container">
<!-- v-model:〇〇 の形式でそれぞれ変数を指定します -->
<ChildComponent
v-model:title="title"
v-model:detail="detail"
/>
</div>
</div>
</template>
<script setup>
// ここで 引数付き defineModel を定義します
const title = defineModel("title");
const detail = defineModel("detail");
</script>
<template>
<div class="child-container">
<h3>子コンポーネント</h3>
<div>
タイトル → <input type="text" v-model="title" />
</div>
<div>
詳細 → <input type="text" v-model="detail" />
</div>
</div>
</template>
↓↓↓
defineModel
はコンポーネント間の双方向データバインディングを簡単に実装するための機能です。
「親と子の両方で同じデータを柔軟に扱いたい」場合に利用すると良いでしょう。
子で定義したメソッドを親で利用したい
defineExpose を使いましょう
<script setup>
import ChildComponent from "@/components/ChildComponent.vue";
import { ref } from "vue";
// ここで ref を定義します
const childRef = ref(null);
const callChildComponent = () => {
childRef.value.increment();
};
</script>
<template>
<div>
<h3>親コンポーネント</h3>
<button @click="callChildComponent">ボタン</button>
<div class="parent-container">
<!-- ここで ref を指定します -->
<ChildComponent
ref="childRef"
/>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
// ここで変数、メソッドを定義します
const count = ref(0);
const increment = () => {
count.value++
};
// ここで変数、メソッドを親で利用できるようにします
defineExpose({
count,
increment
});
</script>
<template>
<div class="child-container">
<h3>子コンポーネント</h3>
<p>定義した count → {{ count }}</p>
</div>
</template>
↓↓↓
defineExpose
は defineEmits
と反対に、子で定義したメソッドを親で呼び出すことができます。子コンポーネント内で起きるイベントが子の要素に依存する場合は子で、親の要素に依存するなら親でメソッドを定義すると良いでしょう。
メソッドだけでなく、子にある変数も親で利用することができます。しかし、それを親の <template>
内で
{{ childRef.count }}
とすると、Cannot read properties of null (reading 'count')
エラーになります。初期値が null なので、ref
属性で子コンポーネントを参照する前に「count プロパティがない」と叱られます。親でも変数を利用する場合は defineProps
や defineModel
の利用を検討する方がいいかもしれませんね。
vue3.5
以降なら useTemplateRef()
を利用できます。
<script setup>
import ChildComponent from "@/components/ChildComponent.vue";
// ここで useTemplateRef をインポートします
import { useTemplateRef } from "vue";
// ここで useTemplateRef ヘルバーを利用します
const childRef = useTemplateRef("child");
const callChildComponent = () => {
childRef.value.increment();
};
</script>
<template>
<div>
<h3>親コンポーネント</h3>
<button @click="callChildComponent">ボタン</button>
<div class="parent-container">
<ChildComponent
ref="child"
/>
</div>
</div>
</template>
変数名をそのまま ref
に指定していた従来と比べ、明示的に "child"
という識別子と ref 属性を一致させられるので、直感的というメリットがあります。
さいごに
各 define~
の API はそれぞれ適した使用方法があるので状況に応じて使い分けるのが良いでしょう。コンポーネント間で上手にデータを受け渡せて、快適な開発を行えるようになりたいです。