2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vue.js Composition API におけるコンポーネント間データ連携のまとめ

Last updated at Posted at 2025-05-05

はじめに

Vue.js とは JavaScript を元にしたフレームワークであり、単一ファイルコンポーネントを利用します。コンポーネントを組み合わせて使うのでコンポーネント間でデータを受け渡すのが重要であり、それについて既に多くの方が記事に起こしています。

数多くの技術記事がありますが、

  • Vue.js のバージョンが上がった
  • Composition API での記述は少ない

のが気がかりです。
よって今回は、現時点での Composition API におけるコンポーネント間データ連携のやり方をまとめようと思います。

Composition API ?

Vue.js には Composition APIOptions API があります。それぞれの違いやメリット・デメリットの詳細は以下のドキュメントを参照していただきたいのですが、簡単にいうと...

  • Composition API<script setup> を使う
  • Options API<script> を使う

という使い分けができます。

https://ja.vuejs.org/guide/extras/composition-api-faq.html

コンポーネント間データ連携のまとめ

コンポーネントを取り込む側を親コンポーネント、取り込まれる側を子コンポーネントといいます。以下のような vue ファイルを記述することで、コンポーネントを利用できます。

ParentComponent.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>

ChildComponent.vue
<script setup>
</script>

<template>
  <div class="child-container">
    <h3>子コンポーネント</h3>
  </div>
</template>

<style scoped>
.child-container {
  height: 20vh;
  border: 5px solid black;
}
</style>

CSSでスタイルを適用しているので、画面は以下のようになります。
↓↓↓

コンポーネント画面.png

以下に各種バージョンを記載します。

  • 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 を使いましょう

ParentComponent.vue
<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>
ChildComponent.vue
<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>

↓↓↓

props例.png

子コンポーネントで defineProps を利用して API を定義することができます。上コードのように<template> で props 内の変数を使うことができます。
<script setup>内で使うときは props.をつけるようにします。

ChildComponent.vue
<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 の戻り値からの分割代入でも変数を利用できます。

ChildComponent.vue
<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 を使いましょう

ParentComponent.vue
<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>
ChildComponent.vue
<script setup>
// ここで emits を定義します
const emit = defineEmits(["counter"]);
</script>

<template>
  <div class="child-container">
    <h3>子コンポーネント</h3>
    <!-- emit でメソッドを実行します -->
    <button @click="emit('counter')">ボタン</button>
  </div>
</template>

↓↓↓

props例.gif

子コンポーネントで defineEmits を利用することで、「ボタン」をクリックしたときに何をするかを親コンポーネントで定義できます。
引数もつけることができます。

ParentComponent.vue
<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>
ChildComponent.vue
<script setup>
const emit = defineEmits(["counter"]);
</script>

<template>
  <div class="child-container">
    <h3>子コンポーネント</h3>
    <!-- 引数をつけています -->
    <button @click="emit('counter', 2)">ボタン</button>
  </div>
</template>

↓↓↓

emit2.gif

defineEmits は子コンポーネントのイベントを親で定義できます。
「子コンポーネントの HTML は汎用的に利用したいが、イベントは親ごとに異なる」場合に利用すると良いでしょう。

親と子で同じ変数を扱いたい

defineModel を使いましょう

ParentComponent.vue
<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>
ChildComponent.vue
<script setup>
// ここで defineModel を定義します
const text = defineModel();
</script>

<template>
  <div class="child-container">
    <h3>子コンポーネント</h3>
    <!-- ここで v-model を指定します -->
    <input type="text" v-model="text" />
  </div>
</template>

↓↓↓

model1.gif

バージョン 3.4 以降、子コンポーネントで defineModel を利用することで、親と子の双方向で同じ変数を動的に扱うことができます。
defineModel の引数に文字列を指定することで複数の双方向バインディングを実装することができます。

ParentComponent.vue
<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>
ChildComponent.vue
<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>

↓↓↓

model2.gif

defineModel はコンポーネント間の双方向データバインディングを簡単に実装するための機能です。
「親と子の両方で同じデータを柔軟に扱いたい」場合に利用すると良いでしょう。

子で定義したメソッドを親で利用したい

defineExpose を使いましょう

ParentComponent.vue
<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>
ChildComponent.vue
<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>

↓↓↓

expose1.gif

defineExposedefineEmits と反対に、子で定義したメソッドを親で呼び出すことができます。子コンポーネント内で起きるイベントが子の要素に依存する場合は子で、親の要素に依存するなら親でメソッドを定義すると良いでしょう。
メソッドだけでなく、子にある変数も親で利用することができます。しかし、それを親の <template> 内で

{{ childRef.count }}

とすると、Cannot read properties of null (reading 'count') エラーになります。初期値が null なので、ref 属性で子コンポーネントを参照する前に「count プロパティがない」と叱られます。親でも変数を利用する場合は definePropsdefineModel の利用を検討する方がいいかもしれませんね。

vue3.5 以降なら useTemplateRef() を利用できます。

ParentComponent.vue
<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 はそれぞれ適した使用方法があるので状況に応じて使い分けるのが良いでしょう。コンポーネント間で上手にデータを受け渡せて、快適な開発を行えるようになりたいです。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?