はじめに (対象読者・この記事でわかること)

この記事は、Vue3 と Pinia(または Vuex)を使ってフロントエンド開発を行っているエンジニア、あるいは JavaScript の基礎は理解しているがステート管理と非同期処理で行き詰まっている方を対象としています。
この記事を読むことで、以下のことができるようになります。

  • API から取得したデータを Pinia ストアに正しく格納する方法
  • コンポーネント側でストアの状態を取得した際に undefined が返ってくる原因の特定
  • ref / reactive の扱いと Vue のリアクティビティの仕組みを踏まえた、実用的な回避策の実装

Vue3 の Composition API と Pinia の組み合わせはモダンなフロントエンド開発の主流です。実務で頻繁に遭遇するこの問題を解決することで、デバッグコストを大幅に削減できます。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • HTML / CSS の基本的な知識
  • JavaScript(ES6 以降)の文法と Promise の概念
  • Vue3 の Composition API(setuprefcomputed)の基本
  • 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 のリアクティビティシステムと非同期処理のタイミング、そして refreactive の違いが関係しています。次の章で、典型的な失敗パターンと正しい実装手順を具体的に見ていきましょう。

正しい実装手順とデバッグポイント

ステップ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 をトリガーし、watchcomputed で変化を監視する方が安全です。

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 のままになる

  • 原因: statenull を設定したまま、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 で型エラーになる (profilenull と推論される)

  • 原因: profile の型が null | User と推論され、直接プロパティにアクセスするとエラーになる。
  • 対策: オプショナルチェーン ?. や Null 合体演算子 ?? を併用し、型ガードを入れる。

解決策の総まとめ

  1. ref で状態を管理
    - state の初期値は null でも問題ないが、ref にすることで Vue が代入を正しくトラッキングします。

  2. 非同期呼び出しは onMounted で開始
    - await せずに副作用として実行し、リアクティブ更新を待つ。

  3. テンプレート側はオプショナルチェーンで安全に
    - {{ userStore.profile?.name ?? '---' }} のように記述し、ロード中の null に備える。

  4. デバッグには 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 のプラグイン機能を使った 永続化モジュール分割 に挑戦してみましょう。

参考資料