はじめに
私は駆け出しのエンジニアで、最近Vueに初めて触れました。
コンポーネント間のデータ共有方法は多種多様であり、各方法の用途について混乱してきたため、比較しまとめようと思いました。
その矢先、下記の非常に有り難い記事に出会ってしまい、一瞬自分がまとめる意義を見失いましたが、
今回は、
- 上記記事で紹介されているデータ共有方法のうち、私が現時点で使用したことのあるものに限定し
- 各方法について再調査後、私なりの分類・言語化で再構成し
- サンプルコードを付けることで
自分なりのチートシートのようなものを作成することとしました。
結論:いつ、どの方法を使うべきか
- データ共有
- コンポーネントローカル
- 親→子の一方向:props
- 親→子孫の一方向(※):provide/inject
- 親子間の双方向:defineModel
- グローバル:状態管理ライブラリ
- コンポーネントローカル
- DOM要素渡し(親→子):slot
- イベント渡し(子→親):emit
※propsと比較して、兄弟間のデータ共有を行いたい場合に有用、親を追跡しにくいことが難点
データ共有
コンポーネントローカル
props
- 親から子への一方向のデータ渡し
- 子で受け入れるデータを定義し、親が呼び出す際にデータを渡す
用途
- 親から子への一方向のデータ渡しをしたい場合
ParentComponent.vue
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue';
const message = 'Hello from parent';
</script>
<template>
<ChildComponent :message="message" />
</template>
ChildComponent.vue
<script setup lang="ts">
import { defineProps } from 'vue';
const props = defineProps<{
message: string;
}>();
</script>
<template>
<div>{{ props.message }}</div>
</template>
provide/inject
- 親から子孫への一方向のデータ渡し
- 親でprovideによりデータを提供し、子孫でinjectにより受け取る
- 中間のコンポーネントは経由する必要がない
用途
- 親からデータを渡す対象コンポーネントの、階層が深い場合
- ただし、深い階層でも使用できる分、provideした親の追跡が不明確になる
- 下記の状態管理ライブラリの使用も検討すべき
- 親からのデータを兄弟間でも共有する場合、子で個別の定義が必要なpropsよりも簡潔
ParentComponent.vue
<script setup lang="ts">
import { provide } from 'vue';
const message = 'Shared message';
provide('messageKey', message);
</script>
<template>
<ChildComponent />
</template>
ChildComponent.vue
<script setup lang="ts">
import { inject } from 'vue';
const message = inject('messageKey');
</script>
<template>
<div>{{ message }}</div>
</template>
defineModel
- 親子間でのデータの双方向共有
- 子でpropsとemitを同時に定義し、親でv-modelによってデータをバインドする
用途
- 親子間で、データを双方向にバインディングしたい場合
ParentComponent.vue
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue';
import { ref } from 'vue';
const sharedValue = ref('');
</script>
<template>
<ChildComponent v-model="sharedValue" />
<p>{{ sharedValue }}</p>
</template>
ChildComponent.vue
<script setup lang="ts">
import { defineModel } from 'vue';
const modelValue = defineModel<string>('modelValue');
</script>
<template>
<input v-model="modelValue">
</template>
グローバル
状態管理ライブラリ(Vuex, Pinia)
- 親子関係によらない、グローバルなデータ管理
- アプリケーション全体でデータを一元管理し、複数のコンポーネント間で共有できるようにする
用途
- アプリケーションが大規模で、propsなどでは煩雑になる場合
- 大規模でも、コンポーネントローカルな共有にはpropsでよい
counterStore.ts
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
const increment = () => {
count.value++;
};
return {
count,
increment
};
});
Component.vue
<script setup lang="ts">
import { useCounterStore } from './store';
const counterStore = useCounterStore();
const { count, increment } = counterStore;
</script>
<template>
<p>{{ count }}</p>
<button @click="increment">Increment</button>
</template>
DOM要素渡し(親→子)
slot
- 親から子にDOM要素を渡す
用途
- DOM要素を渡したい場合
ParentComponent.vue
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue';
</script>
<template>
<ChildComponent>
<p>Content</p>
</ChildComponent>
</template>
ChildComponent.vue
<script setup lang="ts">
</script>
<template>
<div>
<slot></slot>
</div>
</template>
イベント渡し(子→親)
emit
- 子でイベントを発火させ、親で受け取る
- 副次的な効果として、子のデータを親へ渡す
- emitメソッドのオプショナルな引数でデータを渡せる
- あくまで、子のイベント発火に伴って行う、親の処理に必要な材料を渡す役割
一方向のデータフローに違反しない理由
- propsは、親が子にデータを渡すことによって、子の状態を変化させる
- 一方、emitのイベント渡しは、イベントが発火したことの単なる通知に過ぎないし、データ渡しは付属情報の送信である
- それらを利用するかどうかは親に委ねられており、親の状態が強制的に変更されるわけではないので、一方向のデータフローは守られている
用途
- 子のイベント発火を親に渡したい場合
- イベント発火に付属するデータを子から親に渡したい場合
ParentComponent.vue
<script setup lang="ts">
function handleUpdate(data: string) {
console.log(data);
}
</script>
<template>
<ChildComponent @update="handleUpdate" />
</template>
ChildComponent.vue
<script setup lang="ts">
import { defineEmits } from 'vue';
const emit = defineEmits(['update']);
function handleClick() {
emit('update', 'Updated from child');
}
</script>
<template>
<button @click="handleClick">Update Parent</button>
</template>
おわりに
今回調べきれていないデータ共有方法として、expose, scoped slot, グローバルrefもあるようです。
逐次それらについても習得していきたいです。
参考文献