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

# はじめに

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

https://qiita.com/catkk/items/bd474d75b42aae2b92e9

https://qiita.com/att55/items/91b683c68b5057eaac51

https://zenn.dev/sindicum/articles/f9e737fba22281

https://macareux.co.jp/blog/vuejs-1

https://www.heiyoshilog-business.com/vue-js-dataconnect/

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

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

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

<details><summary>Composition API ?</summary>

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

+ **Composition API** → `<script setup>` を使う
+ **Options API** → `<script>` を使う

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

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

</details>

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

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

```vuejs: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>

```

```vuejs: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2405417/0bfb3785-b9e3-433f-8e9f-8c80049e0666.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|

なお、これらは以下の公式ドキュメントから引用していますので、併せてご参照ください。

https://ja.vuejs.org/api/sfc-script-setup


## 親で定義した変数を子で使いたい

:::note
defineProps を使いましょう
:::

```vuejs: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>
```

```vuejs: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2405417/2d148c0b-278d-4fbc-b5f3-fe196aa46163.png)

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

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

```vuejs:ChildComponent.vue
<script setup>

// ここで分割代入
const { title, detail } = defineProps({
  title: {
    type: String
  },
  detail: {
    type: String
  },
});

// 正しく出力されます
console.log(title);
console.log(detail);

</script>
```

props で渡すデータは読み取り専用です。
よって「データは１回渡せればよく、動的に変更する予定がない」場合は `defineProps` を利用すれば良いでしょう。

> defineProps では詳細な定義を行うことを推奨します。
> https://ja.vuejs.org/style-guide/rules-essential.html


## 子で使うイベントを親で定義したい

:::note
defineEmits を使いましょう
:::

```vuejs: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>
```

```vuejs: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2405417/95bad7fe-948d-4317-8c6e-4ae3f5c6815c.gif)


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

```vuejs: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>
```

```vuejs: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2405417/832f8449-1c02-4fbe-b09c-014d7bbb98e9.gif)


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


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

:::note
defineModel を使いましょう
:::

```vuejs: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>
```

```vuejs: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2405417/e5513bc5-8431-45f3-828c-76cc9c524e55.gif)


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

```vuejs: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>
```

```vuejs: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2405417/29108e7b-f0a8-4d43-a350-b678c97335cd.gif)

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


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

:::note
defineExpose を使いましょう
:::

```vuejs: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>
```

```vuejs: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2405417/9420322d-b908-492b-bada-f379923c03be.gif)

`defineExpose` は `defineEmits` と反対に、子で定義したメソッドを親で呼び出すことができます。子コンポーネント内で起きるイベントが子の要素に依存する場合は子で、親の要素に依存するなら親でメソッドを定義すると良いでしょう。
メソッドだけでなく、子にある変数も親で利用することができます。しかし、それを親の `<template>` 内で
```vuejs
{{ childRef.count }}
```
とすると、`Cannot read properties of null (reading 'count')` エラーになります。初期値が null なので、`ref` 属性で子コンポーネントを参照する前に「count プロパティがない」と叱られます。親でも変数を利用する場合は `defineProps` や `defineModel` の利用を検討する方がいいかもしれませんね。

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

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