Vue.js におけるデータ共有の基本要件
データ共有のために重要な2つの要件:
-
リアクティブ性 (Reactivity):
- データが変更された際に、コンポーネントが自動的に更新される必要がある。
- 例えば、ユーザーの操作やバックエンドからのデータ更新があった場合、それに応じて画面が自動的に再描画される
-
型安全性 (Type Safety):
- データが型安全にやりとりされることが重要。
- 特に大規模アプリケーションでは、正しいデータ型を維持することでバグを減らし、開発体験を向上させる。
Vue.js 3 におけるデータ共有の4つの方法
1. Props を使う方法
概要
- 親コンポーネントから子コンポーネントにデータを渡すための最も基本的な方法。
- 子コンポーネントは親コンポーネントから
props
を受け取り、それを使ってデータを表示または操作する。
サンプルコード
親コンポーネント (Parent.vue):
<template>
<Child :message="greeting" />
</template>
<script>
import Child from './Child.vue';
export default {
setup() {
const greeting = 'こんにちは!';
return { greeting };
},
};
</script>
子コンポーネント (Child.vue):
<template>
<div>{{ message }}</div>
</template>
<script>
export default {
props: {
message: String,
},
};
</script>
メリット
- シンプルで、親子関係の明示的なデータバインディングが可能。
- データの流れがわかりやすく、コンポーネント間の依存性が明確。
デメリット
- 深いコンポーネント階層での使用には不向き("props drilling" が発生しやすい)。
2. Provide / Inject + reactive を使う方法
概要
Provide で渡されるデータを ref にして、親からリアクティブに更新できる。
サンプルコード
親コンポーネント (Parent.vue):
<template>
<Child />
<button @click="changeMessage">メッセージ変更</button>
</template>
<script>
import { ref, provide } from 'vue';
import Child from './Child.vue';
export default {
setup() {
const message = ref('こんにちは!');
provide('greeting', message);
const changeMessage = () => {
message.value = 'こんばんは!';
};
return { changeMessage };
},
};
</script>
子コンポーネント (Child.vue):
<template>
<div>{{ greeting }}</div>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
const greeting = inject('greeting');
return { greeting };
},
};
</script>
メリット
- 親コンポーネントでデータが変更されたら、子コンポーネントにリアクティブに反映される。
- リアクティブな状態管理がシンプルに実現できる。
- データ子供の深くに渡せます
デメリット
親子関係に依存するため、コンポーネントの階層が複雑になると管理が難しくなる。
3. グローバル ref を使う方法
概要
- Vue 3 の ref() を使って、どのコンポーネントでもアクセスできるグローバル変数を作る。
サンプルコード
store.js:
import { ref } from 'vue';
export const globalCount = ref(0);
任意のコンポーネント (ComponentA.vue):
<template>
<button @click="increment">増加</button>
</template>
<script>
import { globalCount } from './store.js';
export default {
setup() {
const increment = () => {
globalCount.value++;
};
return { increment };
},
};
</script>
メリット
- 簡単にグローバルな状態管理ができる。
- リアクティブなデータバインディングが可能。
デメリット
- データの管理が増えて、状態が増えると複雑になる。
- 大規模なアプリには不向き。
4. Pinia を使う方法
概要
- Vuex の代わりに使われる軽量なストアライブラリ。
サンプルコード
ストア (store.js):
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++;
},
},
});
任意のコンポーネント (ComponentA.vue):
<template>
<button @click="increment">増加</button>
</template>
<script>
import { useCounterStore } from './store.js';
export default {
setup() {
const store = useCounterStore();
return { increment: store.increment };
},
};
</script>
別のコンポーネント (ComponentB.vue):
<template>
<p>カウント: {{ count }}</p>
</template>
<script>
import { useCounterStore } from './store.js';
export default {
setup() {
const store = useCounterStore();
return { count: store.count };
},
};
</script>
メリット
- シンプル: 状態管理が簡潔で、直感的な API 。
- リアクティブ: 複数のコンポーネント間でリアクティブにデータを共有できる。
- 開発者ツール (Devtool) 対応: 状態の変化を Vue Devtools でデバッグできる。
- 型安全 (Typesafe): TypeScript に対応しており、型安全なストア管理が可能。
- モジュール化可能: 複数のストアを簡単に分けて管理できる。
デメリット
- 小規模なプロジェクトには少しオーバーヘッドになる可能性がある。
結論: どの方法を使うべきか?
個人的に以下のこと
- propsは絶対に使う
- 親子関係が明確かつリアクティブな管理が必要: Provide / Inject + reactive
- 大規模なアプリじゃなければ: グローバルのref
- 大規模アプリや複雑な状態管理: Pinia