はじめに (対象読者・この記事でわかること)
この記事は、Vue3 と Pinia(または Vuex)を使ってフロントエンド開発を行っているエンジニア、あるいは JavaScript の基礎は理解しているがステート管理と非同期処理で行き詰まっている方を対象としています。
この記事を読むことで、以下のことができるようになります。
- API から取得したデータを Pinia ストアに正しく格納する方法
- コンポーネント側でストアの状態を取得した際に
undefinedが返ってくる原因の特定 ref/reactiveの扱いと Vue のリアクティビティの仕組みを踏まえた、実用的な回避策の実装
Vue3 の Composition API と Pinia の組み合わせはモダンなフロントエンド開発の主流です。実務で頻繁に遭遇するこの問題を解決することで、デバッグコストを大幅に削減できます。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- HTML / CSS の基本的な知識
- JavaScript(ES6 以降)の文法と Promise の概念
- Vue3 の Composition API(
setup、ref、computed)の基本 - Pinia(または Vuex)のストア定義と利用方法
APIレスポンスと Pinia ストアの概要
Vue3 で外部 API(例:RESTful な JSON エンドポイント)からデータを取得し、アプリ全体で共有したい場合、Pinia のストアに格納してコンポーネントから参照します。
しかし、以下のようなコードパターンで実装すると、コンポーネント側で取得した値が undefined になるケースが多々報告されています。
Js// store/user.js import { defineStore } from 'pinia' import axios from 'axios' export const useUserStore = defineStore('user', { state: () => ({ profile: null // ← 初期値が null }), actions: { async fetchProfile() { const res = await axios.get('/api/user') this.profile = res.data // ここで代入 } } })
Vue<script setup> import { useUserStore } from '@/store/user' const userStore = useUserStore() await userStore.fetchProfile() console.log(userStore.profile) // ← ここが undefined になることがある </script>
この現象の背後には、Vue のリアクティビティシステムと非同期処理のタイミング、そして ref と reactive の違いが関係しています。次の章で、典型的な失敗パターンと正しい実装手順を具体的に見ていきましょう。
正しい実装手順とデバッグポイント
ステップ1 ストアの状態を ref でラップする
Pinia の state は内部で reactive に変換されますが、非同期で代入したオブジェクトが深層でリアクティブになるかは注意が必要です。特に、初期値が null のまま代入すると Vue が変化を検知できないケースがあります。安全策として、ref を使って明示的にリアクティブにします。
Js// store/user.js import { defineStore } from 'pinia' import { ref } from 'vue' import axios from 'axios' export const useUserStore = defineStore('user', () => { const profile = ref(null) // ref にすることでリアクティブが保証される const fetchProfile = async () => { const { data } = await axios.get('/api/user') profile.value = data // 代入は .value 経由 } return { profile, fetchProfile } })
ステップ2 コンポーネント側で await せずにリアクティブを監視
setup の中で await userStore.fetchProfile() を呼び出すと、コンポーネントのマウントが完了する前に非同期処理が走り、profile がまだ null の状態で描画が行われます。Vue はリアクティブな変更を自動で再描画しますが、初回描画時に undefined が出力されてしまうことがあります。したがって、await せずに fetchProfile をトリガーし、watch か computed で変化を監視する方が安全です。
Vue<script setup> import { useUserStore } from '@/store/user' import { onMounted, computed } from 'vue' const userStore = useUserStore() onMounted(() => { userStore.fetchProfile() // 非同期処理はここで開始 }) // プロファイルが取得できたら computed が再評価され、テンプレートが更新される const userName = computed(() => userStore.profile?.name ?? '---') </script> <template> <div> <p>ユーザー名: {{ userName }}</p> </div> </template>
ハマった点やエラー解決
1. profile が常に null のままになる
- 原因:
stateにnullを設定したまま、this.profile = res.dataと代入したが、res.dataがオブジェクトであっても Vue が深層のプロパティ変更を検知できないケース(特に API が遅延した場合)。 - 対策:
refにラップし、代入はprofile.value = dataとする。
2. await した結果、コンポーネントが未マウント の状態で console.log が走り undefined が出力される
- 原因:
setupが非同期で停止せずに次の行が実行され、profileがまだnull。 - 対策:
awaitを避け、onMountedで非同期処理を開始し、computed/watchでリアクティブに追従させる。
3. TypeScript で型エラーになる (profile が null と推論される)
- 原因:
profileの型がnull | Userと推論され、直接プロパティにアクセスするとエラーになる。 - 対策: オプショナルチェーン
?.や Null 合体演算子??を併用し、型ガードを入れる。
解決策の総まとめ
-
refで状態を管理
-stateの初期値はnullでも問題ないが、refにすることで Vue が代入を正しくトラッキングします。 -
非同期呼び出しは
onMountedで開始
-awaitせずに副作用として実行し、リアクティブ更新を待つ。 -
テンプレート側はオプショナルチェーンで安全に
-{{ userStore.profile?.name ?? '---' }}のように記述し、ロード中のnullに備える。 -
デバッグには
watchを活用
-watch(() => userStore.profile, (newVal) => console.log('profile updated', newVal))とすれば、変更タイミングが明確になります。
これらの手順を守れば、Vue3 + Pinia で API データを取得し、コンポーネントに流す際の undefined 問題はほぼ解消できます。
まとめ
本記事では、Vue3 の Composition API と Pinia を組み合わせた際に、API レスポンスをストアに格納したもののコンポーネントで undefined が返ってくる典型的な原因と、その対策を詳述しました。
- 原因はリアクティブな代入タイミングと
ref/reactiveの扱いにあった - 対策は
refで状態を管理し、onMountedで非同期取得、computedで安全に表示すること - 結果として、データ取得後の再描画が確実に行われ、
undefinedエラーが消滅
この手順を実装すれば、デバッグ時間を削減し、アプリの信頼性を向上させられます。次は、Pinia のプラグイン機能を使った 永続化 や モジュール分割 に挑戦してみましょう。
参考資料
- Pinia 公式ドキュメント – State の定義とリアクティビティ
- Vue.js 公式ガイド – Composition API の基本
- Axios 公式サイト – HTTP リクエストの使い方
- 「Vue.js実践入門」技術評論社、2023年(ISBN: 978-4-7973-XXXXX)