Vue3 - コンポーネント連携(Provide/Inject・Pinia)
Vue.jsにおいてコンポーネント間で値をやり取りする場合に幾つかの方法があります。
- プロパティ:親コンポーネントから子コンポーネントへの伝達
- カスタムイベント:子コンポーネントから親コンポーネントへの伝達
- スロット:親コンポーネントから子コンポーネントへタグ本体を伝達
- Provide/Inject:親コンポーネントから子、孫コンポーネントへの伝達
- Pinia:アプリ全体での状態管理
小規模で単純なコンポーネントであればプロパティとカスタムイベントだけでも何とかなりそうですが規模が大きくなる場合は状態管理をするための方法を選択する必要があります。
今回は、Provide/InjectとPiniaについて基本的な使い方を解説します。
動作確認用のプロジェクトは create-vue
を使って作成しておりフォルダ構成は以下のようになっています。
├── src
│ ├── App.vue
│ ├── assets
│ ├── components
│ ├── main.js
│ ├── router
│ ├── stores
│ └── views
└── vite.config.js
Provide/Inject
App.vue内で作成したコンポーネントを表示させることで表示確認できるようにします。
<script setup>
import ProvideBasic from "./views/ProvideBasic.vue";
</script>
<template>
<ProvideBasic />
</template>
Provideを使って親コンポーネントにて値をセットします。views/ProvideBasic.vueを作成。
<script setup>
import { provide, ref } from "vue";
import InjectList from "../components/InjectList.vue";
const books = ref([
{ title: "aaa", price: 1000 },
{ title: "bbb", price: 2000 },
{ title: "ccc", price: 3000 },
]);
provide("list", books);
</script>
<template>
<InjectList />
</template>
次に子コンポーネント側でInjectを使ってこの値を取り出してみます。components/InjectList.vueを作成。
<script setup>
import { inject } from "vue";
const books = inject("list");
</script>
<template>
<table>
<th>タイトル</th>
<th>価格</th>
<tr v-for="b in books" :key="b.title">
<td>{{ b.title }}</td>
<td>{{ b.price }}</td>
</tr>
</table>
</template>
ブラウザで確認するとセットした値が表示できているはずです。
Pinia
create-vue
を使ってプロジェクト作成をする場合は作成時の質問でPiniaを使うか質問されます。使うを選択するとsrc/storesフォルダが自動で作成されるのでそこに状態管理をするためのファイルを作っていきます。
今回は例としてcounter.jsを作成してみます。初期値が0でボタンを押すと数が増えるというシンプルなものです。
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", {
state: () => ({ count: 0 }),
getters: {
ceiling: (state) => {
const round = 10;
return Math.ceil(state.count / round) * round;
},
},
actions: {
increment() {
this.count++;
},
},
});
defineStoreを使って管理したい値を定義します。
上記の例では以下の内容を意味しています。
- counter:ストアのid
- state:データ本体
- getters:データを取得する仕組み
- actions:データを更新する仕組み
確認用にコンポーネント(views/PiniaBasic.vue)を作成してApp.vueで表示させておきます。
<script setup>
import PiniaBasic from "./views/PiniaBasic.vue";
</script>
<template>
<PiniaBasic />
</template>
<script setup>
import { storeToRefs } from "pinia";
import { useCounterStore } from "../stores/counter";
const store = useCounterStore();
const { count, ceiling } = storeToRefs(store);
const onclick = () => {
store.increment();
};
</script>
<template>
カウンター:{{ count }} / {{ ceiling }}
<input type="button" @click="onclick" value="+" />
</template>
useCounterStore()を使うとstoreオブジェクトを作成できます。
このオブジェクトを利用しstore.increment()のようにactionを使うことができます。
storeToRefs()を使うとリアクティビティを維持しながらストアのデータを抽出することができます。
ブラウザで確認するとボタンのクリックに応じてカウントアップされていくはずです。
まとめ
Vue2のOptions APIでProvide / Injectを使う場合はデフォルトでリアクティブにならないので使い難い感じがあります。Vue3のComposition APIで使う場合はリアクティブなので使い勝手が良くコードもスッキリしていい感じです。
Vue3から推奨される状態管理ライブラリがVuexからPiniaに変わりました。Vuexはmutationの定義が必要ですがPiniaはactionでステートの更新が可能なのでシンプルで使いやすいと思います。新規でアプリを作る場合は積極的に使っていきましょう。
Vue2からVue3への移行やVue3の新規作成が増えてくると思うので普通に使えるようになっておきたいですね。
参考
Vue.js Provide / inject
Composition API Provide / Inject
Pinia
速習 Vue.js 3 - Composition API編 速習シリーズ
Discussion