Go × Vue3 混在プロジェクト②
1. 問題の発端
今回発生したエラーは以下でした。
Uncaught TypeError: Cannot read properties of undefined (reading '_s')
この _s は Pinia が内部で保持している store の Map であり、このエラーは
「アクティブな Pinia が存在しない状態で store を使おうとした」
ことを意味します。
2. なぜ「import しているのに」動かなかったのか
当初のコードでは以下のような構造になっていました。
-
outsideApp.js-
pushReceiveを import -
window.pushReceive = pushReceive -
createApp(Drawer).mount(...)
-
-
pushReceive.js-
threadを import
-
-
thread.js-
useBookmarksStore -
useMessagesStore
-
この構造だけを見ると、
「必要なものはすべて import されている」
ように見えます。しかし、Pinia(および Vue の多くの仕組み)は import だけでは動作しません。
3. Pinia の本質的な仕組み
Pinia は単なる関数ライブラリではなく、実行時に有効なコンテキスト(activePinia)を必要とする状態管理ライブラリです。
Pinia を使うためには、必ず次の手順が必要になります。
const pinia = createPinia()
app.use(pinia)
この処理によって初めて、
-
Pinia インスタンスが生成され
-
Vue アプリケーションに注入され
-
activePiniaが設定される
という状態になります。
useBookmarksStore() や useMessagesStore() は、
「今アクティブな Pinia はどれか?」
を内部的に参照するため、activePinia が存在しないと即座にエラーになります。
4. 今回の決定的な原因
今回の outsideApp.js では、
createApp(Drawer).mount('#drawer_column')
は行われていましたが、
createPinia()
app.use(pinia)
が一切行われていませんでした。
そのため、
-
Vue アプリは存在する
-
しかし Pinia は一度も初期化されていない
-
pushReceiveは Vue の外から呼ばれる -
その中で Pinia store を使う
-
activePinia が存在しないためクラッシュ
という流れが発生していました。
5. なぜ「Vue の外」から呼ぶと問題が顕在化するのか
Vue コンポーネントの setup() 内で store を使う場合、
app.use(pinia)
が済んでいれば、自動的に activePinia が設定されます。
しかし今回の pushReceive は、
-
HTML の inline script
-
window.pushReceive -
Push 通知や IndexedDB 処理
といった Vue のライフサイクル外 から呼ばれていました。
この場合、Pinia は「どのアプリの Pinia を使えばよいか」を判断できないため、明示的に指定する必要があります。
6. 解決策の本質
解決策は以下の 2 点に集約されます。
① Pinia を必ず生成し、Vue アプリに注入する
const pinia = createPinia()
app.use(pinia)
② Vue 外から store を使う入口で activePinia を設定する
import { setActivePinia } from 'pinia'
window.pushReceive = (data, fromPush = false) => {
setActivePinia(pinia)
return pushReceive(data, fromPush)
}
この構成により、
-
Vue コンポーネント内
-
Vue 外(push / IndexedDB / window 経由)
の両方から、同一の Pinia store を安全に利用できるようになります。
7. Pinia 以外にも同様の制約を持つ機能
今回の問題は Pinia に限りません。Vue には「実行時コンテキスト必須」の機能が多数あります。
Vue 外では動かない代表例
-
useRouter()/useRoute()(Vue Router) -
provide / inject -
useI18n()(vue-i18n) -
watch,computed -
getCurrentInstance()
これらはすべて、
Vue アプリまたは plugin コンテキスト内でのみ有効
という共通点を持っています。
8. Vue 外でも安全に使えるもの
一方で、以下は Vue と無関係なため問題ありません。
-
IndexedDB
-
localStorage / sessionStorage
-
fetch / WebSocket
-
Notification / Audio
-
Service Worker
今回の push 処理が IndexedDB + Pinia 更新 という構成なのは、設計として非常に正しいと言えます。
9. 設計上の重要な原則
今回の一連のトラブルから得られる最重要原則はこれです。
Vue 外では「状態の更新」だけを行い、
UI の反映は Vue に任せる
つまり、
pushReceive(Vue外)
↓
Pinia store を更新
↓
Vue コンポーネントが自動的に再描画
という流れが理想形です。
10. 総括
-
importは「読み込み」であって「初期化」ではない -
Pinia は必ず
createPinia()+app.use(pinia)が必要 -
Vue 外から store を使う場合は
setActivePinia()が必須 -
この制約は Vue Router や i18n など多くの機能にも共通
-
今回の解決方法は設計として正しく、拡張性も高い
登録日:
更新日: