Piniaで定義したリアクティブなオブジェクトをVue.jsのコンポーネントで取り出そうとしたのですが、なぜかリアクティブにならずハマったため、備忘録としてまとめます。
問題
Vue.jsとFirebaseを使った個人開発でクイズアプリを作っており、以下のようにクイズカテゴリの取得を試みていました。(console.log
はデバッグ用)
import { defineStore } from "pinia"
import { collection, onSnapshot, query, orderBy } from "firebase/firestore"
import { db } from "@/firebase"
import { ref } from 'vue'
import type { CategoryType } from '@/types'
const categoriesCollectionRef = collection(db, "categories")
export const useQuizStore = defineStore('quiz', () => {
const quizCategories = ref<CategoryType[]>([])
const isLoaded = ref(false);
const getQuizCategories = () => {
// Firestore からカテゴリデータを 'display_order' フィールドの昇順で取得する
const q = query(categoriesCollectionRef, orderBy("display_order", "asc"));
onSnapshot(q, (snapshot) => {
quizCategories.value = snapshot.docs.map((doc) => {
return {
id: doc.id,
title: doc.data().title,
description: doc.data().description,
displayOrder: doc.data().display_order
}
})
console.log('quizCategories in quizStore', quizCategories)
isLoaded.value = true;
})
}
return {
quizCategories,
getQuizCategories,
isLoaded
}
})
<script setup lang="ts">
import { onMounted } from 'vue';
import HomePresentation from './HomePresentation.vue';
import { useQuizStore } from '@/stores/quizStore';
const { quizCategories, getQuizCategories, isLoaded } = useQuizStore();
onMounted(() => {
getQuizCategories();
console.log('quizCategories in HomeContainer', quizCategories)
});
</script>
<template>
<v-container>
<HomePresentation v-if="isLoaded" :quizCategories="quizCategories" />
</v-container>
</template>
しかし画面はいつまで経っても真っ白のままです。
2つのファイルにそれぞれ仕込んだconsole.log
の結果を見てみると、quizStore.ts
のquizCategories
にはカテゴリの配列が格納されているのに、HomeContainer.vue
のquizCategories
にはなぜか値が格納されていません、、。
次のようにHomeContainer.vue
にwatch
を仕込んでみたところ、そもそも'quizCategories updated'
自体がコンソールに出力されなかったことから、「HomeContainer.vue
のquizCategories
がリアクティブになっていない」ということまでは突き止めました。
<script setup lang="ts">
import { onMounted } from 'vue';
import HomePresentation from './HomePresentation.vue';
import { useQuizStore } from '@/stores/quizStore';
import { watch } from 'vue';
const { quizCategories, getQuizCategories, isLoaded } = useQuizStore();
onMounted(() => {
getQuizCategories();
console.log('quizCategories in HomeContainer', quizCategories)
});
// quizCategories を監視し、データ取得が反映されているか確認
watch(quizCategories, (newValue, oldValue) => {
// 'quizCategories updated'が出力されないため、quizCategories がリアクティブになっていない
console.log('quizCategories updated', newValue);
}, { deep: true });
</script>
<template>
<v-container>
<div v-if="isLoaded">
<HomePresentation :quizCategories="quizCategories" />
</div>
</v-container>
</template>
しかし、なぜquizCategories
がリアクティブになっていないのかがわからず、ハマりました、、。
解決策
結論として、以下のように書くことでquizCategories
のリアクティビティが復活しました。
<script setup lang="ts">
import { onMounted } from 'vue';
import HomePresentation from './HomePresentation.vue';
import { useQuizStore } from '@/stores/quizStore';
// 分割代入で取り出すとリアクティビティが失われてしまう
// const { quizCategories, getQuizCategories, isLoaded } = useQuizStore();
// 以下のように取り出す
const quizStore = useQuizStore();
onMounted(() => {
quizStore.getQuizCategories();
});
</script>
<template>
<v-container>
<HomePresentation v-if="quizStore.isLoaded" :quizCategories="quizStore.quizCategories" />
</v-container>
</template>
リアクティブなオブジェクトを、コンポーネント側で分割代入で取り出してしまうと、リアクティビティが失われてしまいます。
そのため、分割代入をつかわず、↑のように取り出す必要があります。
なおこの取り出し方だと、すべてのオブジェクト・メソッドの先頭にquizStore.
をつけなくてはいけないため、冗長に感じる人も多いはずです。
そういう場合は次のようにstoreToRefs
を使うことで、分割代入を実現しつつquizStore.
を付けなくて済むので検討してみてください。
<script setup lang="ts">
import { onMounted } from 'vue';
import HomePresentation from './HomePresentation.vue';
import { useQuizStore } from '@/stores/quizStore';
import { storeToRefs } from 'pinia';
const quizStore = useQuizStore();
// storeToRefs で各プロパティを ref に変換して分割代入を実現する
const { quizCategories, isLoaded } = storeToRefs(quizStore);
onMounted(() => {
quizStore.getQuizCategories();
});
</script>
<template>
<v-container>
<HomePresentation v-if="isLoaded" :quizCategories="quizCategories" />
</v-container>
</template>
ぼくの場合は、取り出すオブジェクトの数が少ないのでstoreToRefs
の採用は見送りました。
補足:Vue.jsの公式ドキュメント
今回の問題は、Piniaにかぎった話ではなくVue.js全体に当てはまる内容です。
Vue.jsの公式ドキュメントには以下の記載がありました。
リアクティブオブジェクトのプロパティをローカル変数に割り当てたり分割代入した場合、その変数へのアクセスや代入は、ソースオブジェクトの get / set プロキシートラップをトリガーしなくなるため、非リアクティブになります。
Vue におけるリアクティビティーの仕組み
分割代入できない: また、リアクティブなオブジェクトのプリミティブ型のプロパティをローカル変数に分割代入したり、そのプロパティを関数に渡したりすると、下記に示すようにリアクティブなつながりが失われることとなります:
reactive() の制限
おわりに
Vue.jsの根本原理への深い理解が自分には不足しているように思いました。
このあたりの学習はどう進めていけばいいか、迷い中です。
コメント