Vue 3 Pinia vs Vuex徹底比較:2026年の状態管理と面接対策ガイド

Vue 3におけるPiniaとVuexの違いを徹底的に比較します。Options StoreとSetup Store、TypeScript統合、ストア間通信、Vuexからの移行手順、SSR対応まで、2026年の面接で問われる状態管理の知識を実践的なコード例とともに解説します。

Vue 3 Pinia vs Vuex state management comparison

Vue 3のエコシステムにおいて、状態管理ライブラリの選択はアプリケーション設計の根幹を左右する重要な決断です。Vuex 4が長年にわたりVue開発者の標準ツールとして機能してきましたが、Vue 3のComposition APIと完全に統合されたPiniaが公式推奨の状態管理ライブラリとなり、2026年現在ではPinia 3がデファクトスタンダードとしての地位を確立しています。面接の場においても、PiniaとVuexの設計思想の違い、移行戦略、そしてTypeScript統合に関する質問が頻出しており、両者の深い理解が求められます。

面接官が注目するポイント

Pinia vs Vuexの面接質問では、単純な構文の違いだけでなく、なぜPiniaがVuexの後継として設計されたのか、その設計思想の転換点を説明できることが重要です。Mutationsの廃止、フラットなストア構造、Composition APIとの自然な統合が、それぞれどのような問題を解決するのかを論理的に説明できる能力が評価されます。

PiniaとVuexの基本的な違い

Piniaは、Vuexの開発経験から得られた教訓を基に一から設計された状態管理ライブラリです。最大の変更点は、Vuexで必須だったMutationsの完全な廃止です。Vuexでは状態の変更に必ずMutationを経由する必要がありましたが、Piniaではアクション内で状態を直接変更できます。この設計変更により、定型コードが大幅に削減され、開発者の生産性が向上します。

また、Vuexが単一のストアにネストされたモジュールを持つ構造を採用していたのに対し、Piniaは複数の独立したフラットなストアで構成されます。各ストアが自律的に動作するため、コード分割が容易になり、必要なストアだけをインポートする明確な依存関係が実現されます。

以下の比較表は、両者の主要な違いを整理したものです。

| 機能 | Pinia 3 | Vuex 4 | |------|---------|--------| | Mutations | なし(直接的な状態変更) | 状態変更に必須 | | TypeScript | 完全な型推論、型拡張不要 | 手動での型拡張が必要 | | ストア構造 | 複数のフラットなストア | ネストされたモジュールを持つ単一ストア | | Composition API | ネイティブ対応 | Options APIベース | | バンドルサイズ | 約1 KB(gzip圧縮後) | 約6 KB(gzip圧縮後) | | Vue Devtools | 完全対応(v7) | 完全対応 | | SSR | 組み込みサポート | 設定が必要 | | ホットモジュール置換 | 組み込みサポート | 手動設定が必要 |

Options StoreとSetup Store:2つの定義方法

Piniaでは、ストアを定義する方法が2つ用意されています。Options StoreはVuexに慣れた開発者にとって親しみやすい構文を提供し、Setup StoreはComposition APIと同一のパターンで記述できます。

Options Store構文

Options Storeでは、stategettersactionsを明示的に分離して定義します。Vuexからの移行時に最も直感的な選択肢となります。

stores/counter-options.tstypescript
import { defineStore } from 'pinia'

// Options Store syntax — familiar to Vuex developers
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    lastUpdated: null as Date | null,
  }),
  getters: {
    // Getters receive state as first argument with full type inference
    doubleCount: (state) => state.count * 2,
    isPositive(): boolean {
      // Access other getters via `this`
      return this.count > 0
    },
  },
  actions: {
    increment() {
      // Direct state mutation — no commit() needed
      this.count++
      this.lastUpdated = new Date()
    },
    async fetchCount(id: string) {
      // Async actions work without extra configuration
      const response = await fetch(`/api/counters/${id}`)
      const data = await response.json()
      this.count = data.count
    },
  },
})

Vuexとの重要な違いとして、incrementアクション内でthis.count++と直接状態を変更している点が挙げられます。Vuexではcommit('INCREMENT')のようにMutationを経由する必要がありましたが、Piniaではこの間接的なレイヤーが不要です。非同期処理も特別な設定なしでアクション内に直接記述できます。

Setup Store構文

Setup Storeは、Vue 3のComposition APIと同一のパターンでストアを定義します。ref()が状態に、computed()がゲッターに、通常の関数がアクションに対応します。

stores/counter-setup.tstypescript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// Setup Store syntax — identical patterns to Composition API
export const useCounterStore = defineStore('counter', () => {
  // ref() becomes state
  const count = ref(0)
  const lastUpdated = ref<Date | null>(null)

  // computed() becomes getters
  const doubleCount = computed(() => count.value * 2)
  const isPositive = computed(() => count.value > 0)

  // Functions become actions
  function increment() {
    count.value++
    lastUpdated.value = new Date()
  }

  async function fetchCount(id: string) {
    const response = await fetch(`/api/counters/${id}`)
    const data = await response.json()
    count.value = data.count
  }

  // Must return all state, getters, and actions
  return { count, lastUpdated, doubleCount, isPositive, increment, fetchCount }
})

Setup Storeの最大の利点は、Composition APIで習得した知識がそのまま状態管理にも適用できる点です。watchwatchEffect、さらにはVueUseのようなサードパーティのコンポーザブルをストア内部で直接利用できます。

TypeScript統合の比較

TypeScriptとの統合品質は、PiniaとVuexの間で最も顕著な差が現れる領域です。Vuex 4ではTypeScriptの型安全性を確保するために、手動での型拡張が必要でした。

typescript
// Vuex 4 — manual type augmentation required
import { Store } from 'vuex'

declare module 'vuex' {
  interface State {
    counter: {
      count: number
      lastUpdated: Date | null
    }
  }
}

// Accessing state requires type assertions or custom helpers
const count = (store.state as { counter: { count: number } }).counter.count

このコードが示すように、Vuexでは型情報を別途宣言し、状態へのアクセス時に型アサーションやカスタムヘルパーが必要になる場面が頻繁に発生します。プロジェクトの規模が大きくなるほど、この型拡張の管理が複雑化し、保守コストが増大します。

一方、Piniaではこのような型の手動設定が一切不要です。

typescript
// Pinia — full type inference, zero configuration
const counter = useCounterStore()

// TypeScript knows counter.count is number
// TypeScript knows counter.doubleCount is number
// TypeScript knows counter.increment() returns void
// TypeScript knows counter.fetchCount() returns Promise<void>
counter.increment()
console.log(counter.doubleCount)

Piniaでは、ストアの定義から完全な型推論が自動的に行われます。状態の型、ゲッターの戻り値型、アクションのシグネチャがすべて推論され、追加の型宣言は不要です。この「設定不要の型安全性」は、大規模なVueプロジェクトにおける開発体験を大幅に改善します。

Vue.js / Nuxt.jsの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

コンポーザブルとの統合パターン

Setup Storeの設計により、Piniaのストアはサードパーティのコンポーザブルと自然に統合できます。以下の例では、VueUseのuseDebounceFnとVue RouterのuseRouteをストア内部で直接使用しています。

stores/search.tstypescript
import { defineStore } from 'pinia'
import { ref, watch } from 'vue'
import { useDebounceFn } from '@vueuse/core'
import { useRoute } from 'vue-router'

export const useSearchStore = defineStore('search', () => {
  const route = useRoute()
  const query = ref('')
  const results = ref<SearchResult[]>([])
  const isLoading = ref(false)

  // VueUse composable used directly inside the store
  const debouncedSearch = useDebounceFn(async (term: string) => {
    if (!term.trim()) {
      results.value = []
      return
    }
    isLoading.value = true
    try {
      const res = await fetch(`/api/search?q=${encodeURIComponent(term)}`)
      results.value = await res.json()
    } finally {
      isLoading.value = false
    }
  }, 300)

  // Watcher syncs URL query param to store state
  watch(() => route.query.q, (q) => {
    if (typeof q === 'string') {
      query.value = q
      debouncedSearch(q)
    }
  })

  function setQuery(term: string) {
    query.value = term
    debouncedSearch(term)
  }

  return { query, results, isLoading, setQuery }
})

このパターンが示す重要な点は、ストアが単なるデータの保管場所ではなく、ビジネスロジックのカプセル化にも活用できることです。URLクエリパラメータとの同期、デバウンス処理、API通信がストア内で一元管理され、コンポーネント側のコードがシンプルに保たれます。

ストア間通信

Piniaのフラットなストア構造では、あるストアから別のストアにアクセスする場面が発生します。各ストアはコンポーザブルとしてエクスポートされるため、関数呼び出しによって他のストアを取得できます。

stores/cart.tstypescript
import { defineStore } from 'pinia'
import { useProductStore } from './products'
import { useAuthStore } from './auth'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])

  const total = computed(() => {
    // Access another store by calling its composable
    const productStore = useProductStore()
    return items.value.reduce((sum, item) => {
      const product = productStore.getById(item.productId)
      return sum + (product?.price ?? 0) * item.quantity
    }, 0)
  })

  async function checkout() {
    const auth = useAuthStore()
    if (!auth.isAuthenticated) {
      throw new Error('Authentication required')
    }
    // Checkout logic
  }

  return { items, total, checkout }
})

Vuexのネストされたモジュール構造では、rootGettersdispatch('moduleName/actionName', null, { root: true })のような冗長な構文が必要でした。Piniaでは、必要なストアのコンポーザブルをインポートして呼び出すだけで、型安全なストア間通信が実現されます。循環依存が発生する可能性がある場合は、ゲッターやアクションの内部でストアを取得することで回避できます。

VuexからPiniaへの移行手順

既存のVuexプロジェクトをPiniaに移行する作業は、段階的に進めることが推奨されます。PiniaとVuexは同一のアプリケーション内で共存できるため、モジュール単位で順次移行する戦略が現実的です。

移行前:Vuexモジュール

typescript
// Before: Vuex module
// store/modules/user.ts
const userModule = {
  namespaced: true,
  state: () => ({
    profile: null as UserProfile | null,
    preferences: {} as UserPreferences,
  }),
  mutations: {
    SET_PROFILE(state, profile: UserProfile) {
      state.profile = profile
    },
    UPDATE_PREFERENCES(state, prefs: Partial<UserPreferences>) {
      state.preferences = { ...state.preferences, ...prefs }
    },
  },
  actions: {
    async fetchProfile({ commit }) {
      const profile = await api.getProfile()
      commit('SET_PROFILE', profile)
    },
    async updatePreferences({ commit }, prefs: Partial<UserPreferences>) {
      await api.updatePreferences(prefs)
      commit('UPDATE_PREFERENCES', prefs)
    },
  },
  getters: {
    isLoggedIn: (state) => state.profile !== null,
    displayName: (state) => state.profile?.name ?? 'Guest',
  },
}

移行後:Piniaストア

typescript
// After: Pinia store
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { api } from '@/lib/api'

export const useUserStore = defineStore('user', () => {
  const profile = ref<UserProfile | null>(null)
  const preferences = ref<UserPreferences>({})

  const isLoggedIn = computed(() => profile.value !== null)
  const displayName = computed(() => profile.value?.name ?? 'Guest')

  async function fetchProfile() {
    profile.value = await api.getProfile()
  }

  async function updatePreferences(prefs: Partial<UserPreferences>) {
    await api.updatePreferences(prefs)
    Object.assign(preferences.value, prefs)
  }

  return { profile, preferences, isLoggedIn, displayName, fetchProfile, updatePreferences }
})

移行時の主要な変更点を整理すると、以下の通りです。まず、Mutationsが完全に廃止され、アクション内で状態を直接更新します。次に、commit()呼び出しが不要になり、thisやrefへの直接代入に置き換わります。また、名前空間付きモジュールが独立したストアファイルに変換され、ゲッターはcomputed()に移行します。

SSRとNuxtにおけるPinia

PiniaはSSR(サーバーサイドレンダリング)を組み込みでサポートしています。Nuxt 4では@pinia/nuxtモジュールにより自動的に統合され、追加の設定は最小限に抑えられます。

stores/products.ts — SSR-safe Pinia store for Nuxt 4typescript
import { defineStore } from 'pinia'

export const useProductStore = defineStore('products', {
  state: () => ({
    items: [] as Product[],
    selectedCategory: 'all',
  }),
  actions: {
    async fetchProducts() {
      // useFetch is Nuxt-specific — use $fetch for store actions
      this.items = await $fetch<Product[]>('/api/products', {
        query: { category: this.selectedCategory },
      })
    },
  },
  getters: {
    getById: (state) => (id: string) =>
      state.items.find((item) => item.id === id),
    filteredCount: (state) =>
      state.items.filter((p) => p.inStock).length,
  },
})

SSR環境における重要な注意点として、ストアアクション内ではuseFetchではなく$fetchを使用します。useFetchはVueコンポーネントのsetup()内でのみ動作するNuxt固有のコンポーザブルであり、ストアアクション内では$fetchが適切な選択です。Piniaの組み込みSSRサポートにより、サーバーで取得した状態がクライアントへ自動的にハイドレートされ、データの重複取得が回避されます。

SSRでのストア初期化に関する注意

SSR環境ではストアがリクエストごとに新しいインスタンスとして作成される必要があります。グローバルなシングルトンとしてストアを定義すると、リクエスト間で状態が漏洩する危険性があります。Nuxtの@pinia/nuxtモジュールはこの問題を自動的に処理しますが、カスタムSSR環境ではPiniaインスタンスのリクエストスコープ管理に注意が必要です。

面接で頻出するPinia vs Vuexの質問

「PiniaがMutationsを廃止した理由を説明してください」

VuexにおけるMutationsの本来の目的は、Vue Devtoolsによる状態変更の追跡を可能にすることでした。しかし、実際の開発では「アクションからMutationを呼び出し、MutationがStateを変更する」という間接的なフローが冗長なボイラープレートを生み出していました。Piniaでは、Vue 3のリアクティビティシステムの進化により、Devtoolsがアクション内の直接的な状態変更も追跡できるようになったため、Mutationsという中間レイヤーが不要になりました。

「Options StoreとSetup Storeの使い分けを教えてください」

Options Storeは、VuexやOptions APIに慣れたチームにとって学習コストが低く、既存のコードベースからの移行が容易です。Setup Storeは、Composition APIのパターンと一貫性があり、コンポーザブルの直接利用やより柔軟なロジック構成が可能です。新規プロジェクトではSetup Storeが推奨されますが、チームの技術スタックと経験に応じて選択すべきです。

「大規模アプリケーションでPiniaのストアをどのように構成しますか」

Piniaではドメインごとにストアを分離する設計が推奨されます。例えば、認証用のuseAuthStore、カート用のuseCartStore、商品用のuseProductStoreをそれぞれ独立したファイルとして管理します。ストア間の依存関係は、ゲッターやアクション内部で他のストアのコンポーザブルを呼び出すことで解決します。VuexのrootGettersのようなグローバルな参照機構は不要です。

「PiniaのTypeScript統合がVuexより優れている具体的な理由は何ですか」

PiniaはdefineStoreの定義からすべての型を自動推論します。状態のプロパティ型、ゲッターの戻り値型、アクションの引数と戻り値型がすべてTypeScriptコンパイラにより推論されるため、declare moduleによる手動の型拡張が不要です。これにより、型定義の保守コストがゼロになり、リファクタリング時の型安全性も自動的に保証されます。

「VuexからPiniaへの移行戦略について説明してください」

PiniaとVuexは同一アプリケーション内で共存できるため、モジュール単位での段階的移行が最も安全な戦略です。各Vuexモジュールを順次Piniaストアに変換し、コンポーネント側の参照を更新します。すべてのモジュールの移行が完了した時点でVuexをアンインストールします。移行中は、新機能の開発をPiniaストアで行い、既存のVuexモジュールは変更を最小限に留める方針が効果的です。

まとめ

本記事で解説したPinia vs Vuexの知識を、面接準備の観点から整理します。

  • PiniaはVue 3の公式推奨状態管理ライブラリであり、Vuex 4の実質的な後継です。新規プロジェクトではPiniaの採用が前提となり、面接でもPiniaの深い理解が求められます。
  • Mutationsの廃止は、Piniaの最も重要な設計判断です。アクション内での直接的な状態変更により、ボイラープレートが削減され、開発体験が向上します。この設計変更の技術的背景を説明できることが面接では重要です。
  • TypeScript統合の品質差は、大規模プロジェクトでの開発効率に直接影響します。Piniaの設定不要の型推論とVuexの手動型拡張の違いを、具体的なコード例とともに説明できるようにしておくことが推奨されます。
  • Setup StoreとComposition APIの統合パターンは、Piniaの高度な活用方法として面接で評価されるポイントです。コンポーザブルのストア内利用やストア間通信の設計を理解しておく必要があります。
  • VuexからPiniaへの移行経験は、実務能力を示す重要な指標です。段階的移行の戦略、共存期間の管理、移行時の注意点について具体的に説明できることが望ましいです。
  • SSRとNuxtにおけるPiniaの挙動は、フルスタックVue開発者にとって必須の知識です。サーバーサイドでのストアインスタンス管理と状態のハイドレーションについて理解しておくことが重要です。

Vue.js / Nuxt.jsの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

タグ

#vue
#pinia
#vuex
#state-management
#interview

共有

関連記事