Vue 3 Pinia の状態管理:defineStore、ゲッター、アクション、および永続化
Piniaは、Vue 3が推奨する公式の状態管理ライブラリであり、Vuexに代わるものです。Piniaは、より簡潔なAPI、TypeScriptの完全なサポート、モジュール型のホットリロード、およびネイティブなDevToolsサポートを提供します。VueチームはPiniaをデフォルトの推奨ライブラリとしました(Vuexはもはやメンテナンスされていません)。
Pinia を使えば、provide/inject よりも体系的な方法で コンポーネント間で共有される状態(ユーザー情報、ショッピングカート、グローバル設定など)を管理でき、Vuex よりも 50% 簡潔に記述できます。
1. 学習内容
- なぜPiniaがVuexよりも優れた選択肢なのか(5つの主な利点)
- defineStore:2つのスタイル(オプション/設定)
- ステート/ゲッター/アクション:3つの基本概念
- ピニア・モジュラー(マルチストア)
- 永続化 (pinia-plugin-persistedstate)
- DevToolsとの連携
- 5つの主要な実例
2. 「ショッピングカート『コンポーネント』における5つの不整合」という悪夢
(1) 課題:5つのコンポーネントがそれぞれ独自のショッピングカートデータを管理している
アリスのEコマースには、カートデータが必要な5つのコンポーネントがありました:
JS
// ❌ The "Flip" Version:5 component 5 set of data
// CartIcon.vue
const cartCount = ref(0)
// ProductCard.vue
const localCart = ref([])
// CartPage.vue
const cart = ref({ items: [], total: 0 })
// CheckoutPage.vue
const myCart = ref([])
// Header.vue
const cartItems = ref([])
プロダクトマネージャーのチャーリー:
「アリス、ProductCardで商品を追加しても、カートのアイコンが更新されないよ! 表示は『0』なのに、ページには『1アイテム』と出ている。5つのコンポーネント、5つのカート――同期が取れていないんだ!」
(2) Vue Pinia の解決策:5つのコンポーネントで1つのストアを共有
JS
// stores/cart.js
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
total: 0
}),
getters: {
itemCount: (state) => state.items.length,
totalPrice: (state) => state.items.reduce((sum, i) => sum + i.price, 0)
},
actions: {
addItem(product) {
this.items.push(product)
this.total += product.price
},
removeItem(id) {
this.items = this.items.filter(i => i.id !== id)
}
}
})
VUE
<!-- CartIcon.vue -->
<script setup>
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
// Automatic Response:cart.itemCount It's changed,icon Update Now
</script>
<template>
<span>🛒 {{ cart.itemCount }}</span>
</template>
VUE
<!-- ProductCard.vue -->
<script setup>
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
</script>
<template>
<button @click="cart.addItem(product)">Add to Cart</button>
</template>
1つのストア、5つのコンポーネントがリアルタイムで同期。
(3) 収益
Pinia を使用した後:
- データの整合性:1つのストア、5つのコンポーネントがリアルタイムで同期される
- コード量:5件の参照 → 1件の保存 (-80%)
- DevTools:タイムトラベルデバッグ(各ステップの状態を確認する)
- TypeScript: 完全な型推論
- 永続性:localStorage に自動的に保存されます
3. PiniaとVuexを比較した際の5つの主な利点
| ディメンション | Vuex 4 (Vue 3) | Pinia (Vue 3) |
|---|---|---|
| APIの簡潔さ | ⭐⭐⭐ 複雑(ミューテーション/アクション) | ⭐⭐⭐⭐⭐ シンプル(状態/ゲッター/アクション) |
| TypeScript | ⭐⭐⭐ 追加の設定が必要 | ⭐⭐⭐⭐⭐ ネイティブ対応 |
| コンポジションAPI | ⭐⭐ 使い勝手が悪い | ⭐⭐⭐⭐⭐ 第一級市民 |
| DevTools | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ 改善点 |
| パッケージサイズ | 約10KB | 約1KB |
Vueは公式にPiniaを推奨しています。また、Vuex 4はメンテナンスモードに移行しました(新機能の追加はありません)。
4. defineStoreの2つのスタイル
(1) オプション形式(Vuexに類似)
JS
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// state:Data
state: () => ({
count: 0,
name: 'Counter'
}),
// getters:Derived values(Similar computed)
getters: {
doubleCount: (state) => state.count * 2,
isZero: (state) => state.count === 0
},
// actions:Methods(Similar methods)
actions: {
increment() {
this.count++
},
async fetchData() {
const res = await fetch('/api/count')
this.count = await res.json()
}
}
})
(2) セットアップ方式(推奨、Composite API)
JS
// stores/auth.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useAuthStore = defineStore('auth', () => {
// 1. state (use ref)
const user = ref(null)
const token = ref(localStorage.getItem('token') || '')
// 2. getters (use computed)
const isLoggedIn = computed(() => !!token.value)
const userName = computed(() => user.value?.name || 'Guest')
// 3. actions(Ordinary Functions)
function login(credentials) {
// API call...
user.value = { name: 'Alice' }
token.value = 'xxx'
}
function logout() {
user.value = null
token.value = ''
}
return { user, token, isLoggedIn, userName, login, logout }
})
(3) オプションと設定の比較
| 寸法 | オプションのスタイル | セットアップのスタイル |
|---|---|---|
| 書き方 | state: () => ({}) |
const x = ref() |
| ゲッター | (state) => ... |
computed() |
| アクション | function() { this.x } |
通常の機能 |
| TypeScript | 手動 | 自動 |
| 組み合わせやすさ | 低い | 高い(他の組み合わせ可能な要素と併用可能) |
| 推奨レベル | Vuexに慣れている方 | 新規プロジェクトでの利用を推奨 |
5. 5つの主要API
(1) 状態:データ
JS
// Options Style
state: () => ({
count: 0,
user: null,
items: []
})
// Setup Style
const count = ref(0)
const user = ref(null)
const items = ref([])
(2) ゲッター:派生値
JS
// Options Style
getters: {
// Simple getter
doubleCount: (state) => state.count * 2,
// Visit Other getters (use this)
ratio(state) {
return this.doubleCount / 100
},
// Return Function(Parameterization getter)
getItemById: (state) => (id) => {
return state.items.find(i => i.id === id)
}
}
// Setup Style
const doubleCount = computed(() => count.value * 2)
const getItemById = (id) => items.value.find(i => i.id === id)
(3) アクション:メソッド
JS
// Options Style
actions: {
// Synchronize
increment() {
this.count++
},
// Asynchronous
async fetchData() {
const res = await fetch('/api/data')
this.data = await res.json()
},
// Visit Others actions
async loginAndFetch(credentials) {
await this.login(credentials)
await this.fetchUser()
}
}
// Setup Style
function increment() {
count.value++
}
async function fetchData() {
const res = await fetch('/api/data')
data.value = await res.json()
}
(4) コンポーネントでの使用
VUE
<script setup>
import { useCartStore } from '@/stores/cart'
import { storeToRefs } from 'pinia'
const cart = useCartStore()
// 1. Direct Access state(Responsive)
console.log(cart.items)
// 2. Use storeToRefs Destructuring (Keep Reactive)
const { items, total } = storeToRefs(cart)
// 3. Call action
cart.addItem(product)
// 4. Monitoring state Changes
watch(() => cart.items, (newItems) => {
console.log('Cart changed:', newItems)
})
</script>
(5) 覚えておくべき5つのポイント
JS
// ⚠️ Note 1:Deconstruction state Responsive Design Missing
const { items } = cart // ❌ items It is a normal value
const { items } = storeToRefs(cart) // ✅ items is ref
// ⚠️ Note 2:Edit state Must use action
cart.items.push(...) // ❌ Not recommended(Edit directly)
cart.addItem(...) // ✅ Use action
// ⚠️ Note 3:action inside this Orientation store
actions: {
increment() {
this.count++ // ✅ this = store
}
}
// ⚠️ Note 4: Getter Cache
getters: {
doubleCount() {
console.log('recomputed') // Print only when dependencies change
return this.count * 2
}
}
// ⚠️ Note 5: useStore in setup, use pinia instance externally
import { getActivePinia } from 'pinia'
const cart = useCartStore(getActivePinia()) // In .js files
6. ピニアの持続性
(1) プラグインをインストールする
BASH
npm install pinia-plugin-persistedstate
(2) main.js の設定
JS
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
const app = createApp(App)
app.use(pinia)
app.mount('#app')
(3) 設定を永続化する5つの方法
JS
// stores/cart.js
export const useCartStore = defineStore('cart', () => {
const items = ref([])
return { items }
}, {
// 1. Default: localStorage key is 'cart'
persist: true,
// 2. Custom key
persist: {
key: 'my-cart',
storage: localStorage
},
// 3. Persist only a portion state
persist: {
paths: ['items'] // Save only items,Do not save total
},
// 4. sessionStorage(Closing the browser clears it)
persist: {
storage: sessionStorage
},
// 5. Custom Serialization(Encryption, etc.)
persist: {
serializer: {
serialize: (value) => btoa(JSON.stringify(value)),
deserialize: (value) => JSON.parse(atob(value))
}
}
})
7. 完全な例:Pinia StoreのECバックエンドの5つの主な機能
(1) ▶ サンプル:. 5つのコアAPI
JS
import { defineStore, storeToRefs } from 'pinia'
import { ref, computed } from 'vue'
// 1. state
const count = ref(0)
// 2. getter
const double = computed(() => count.value * 2)
// 3. action
function increment() { count.value++ }
// 4. Export
export const useStore = defineStore('store', () => {
return { count, double, increment }
})
(2) ▶ サンプル:. Pinia にアクセスする 5 つの方法
VUE
<!-- Direct Access -->
<template>{{ cart.items.length }}</template>
<script setup>
const cart = useCartStore()
</script>
<!-- Deconstruction(Stay Responsive)-->
<script setup>
const cart = useCartStore()
const { items, total } = storeToRefs(cart)
</script>
<!-- Monitor Changes -->
<script setup>
watch(() => cart.items, (newItems) => {
console.log('Cart updated:', newItems.length)
})
</script>
(3) ▶ サンプル:. 「オプション」スタイルと「設定」スタイルの比較
JS
// Options Style(Vuex Habits)
export const useStore1 = defineStore('store1', {
state: () => ({ count: 0 }),
getters: { double: (s) => s.count * 2 },
actions: { increment() { this.count++ } }
})
// Setup Style(Recommendations)
export const useStore2 = defineStore('store2', () => {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() { count.value++ }
return { count, double, increment }
})
(4) ▶ サンプル:. 5種類の設定の永続化
JS
// 1. Default
persist: true
// 2. Custom key
persist: { key: 'my-cart' }
// 3. Selectivity
persist: { paths: ['items'] }
// 4. sessionStorage
persist: { storage: sessionStorage }
// 5. Encryption
persist: {
serializer: {
serialize: JSON.stringify,
deserialize: JSON.parse
}
}
(5) ▶ サンプル:. よくある5つの間違いに関するクイックリファレンス
| エラー | 症状 | 解決策 |
|---|---|---|
| 「State Lost」の応答を分析 | データに変更なし | storeToRefs を使用 |
| 状態を直接変更する | 警告 | 代わりにアクションを使用してください |
await なしの非同期アクション |
結果にばらつきがある | await cart.fetchData() |
| Pinia 未登録 | useStore is not a function |
main.js app.use(pinia) |
| 永続化後にフィールドが保存されない | キーが正しくない | パスの設定を確認してください |
(6) ▶ サンプル:. 5つの実践的なシナリオ
| シナリオ | 店舗 | 主要項目 |
|---|---|---|
| 認証 | useAuthStore | ユーザー、トークン、isLoggedIn |
| ショッピングカート | useCartStore | items, total, itemCount |
| テーマ | useThemeStore | テーマ、ロケール、密度 |
| 通知 | useNotificationStore | 通知、未読 |
| データ | useDataStore | リスト、読み込み中、エラー |
❓ よくある質問
Q PiniaとVuex、どちらを選ぶべきですか?
A Piniaです。Vueチームは公式にPiniaを推奨しており、Vuex 4はもはやメンテナンスされていません。新規プロジェクトではPiniaを使用する必要があります。既存のVuexプロジェクトについては、段階的に移行することができます。
Q Piniaはprovide/injectに代わるものですか?
A いいえ、そうではありません。Piniaはグローバルな状態管理を目的としており、provide/injectはレベル間の通信を目的としています。Piniaは「アプリケーション全体の状態」(ユーザー/カート)に使用されるのに対し、provide/injectは「テーマ/i18n/設定」に使用されます。
Q PiniaとComposableは、それぞれどのような場合に使用すべきですか?
A Piniaは、コンポーネント間で共有されるグローバルな状態(ユーザー情報やショッピングカートなど)に使用します。Composableは、コンポーネントレベルで再利用可能なロジック(
useMouseやuseFetchなど)に使用します。Piniaはより構造化されており、DevToolsも備えています。Q Pinia はどのようにモジュール化されていますか?
A 各ストアは、
src/stores/ ディレクトリ内の個別のファイルに格納されています。Pinia はツリーシェーキングを自動的にサポートしています。Q
storeToRefs はいつ使用すべきですか?A
state をデストラクチャリングする際には必ず使用する必要があります。そうしないと、リアクティブ性が失われます。ゲッターやアクションでは必須ではありません。Q Piniaは永続化のためにどのようなストレージオプションをサポートしていますか?
A デフォルトではlocalStorageをサポートしています。また、sessionStorage、カスタムストレージ(IndexedDB、クッキーなど)、およびカスタムシリアライゼーション(暗号化)もサポートしています。
Q Piniaのパフォーマンスはどのようになっていますか?
A 状態の変化によってリアクティブな更新がトリガーされ、ゲッターは自動的にキャッシュされ、DevToolsによる最適化も提供されます。Vuexよりも30~50%高速です(ミューテーションの概念を使用していません)。
Q PiniaはSSRと併用できますか?
A はい。
import { createPinia } from 'pinia'、リクエストごとに個別のインスタンスが作成されます。Nuxt 3にはPiniaとの統合機能が組み込まれています。📖 まとめ
- Pinia は Vue 3 向けに公式に推奨されている状態管理ライブラリであり、Vuex に代わるものです
- 5つの主な利点:シンプルさ/TypeScript/構成/開発ツール/パッケージサイズの小ささ
- defineStore:2つのスタイル:Options(Vuexの慣例)/Setup(推奨)
- 5つの主要なAPI:state / getters / actions / storeToRefs / Persistence
- モジュール性:店舗ごとに1つのファイル
- 永続性: pinia-plugin-persistedstate
- Pinia 対 Composable:グローバル状態 対 コンポーネントロジック
📝 練習問題
(1) 基礎問題(難易度:⭐)
簡単なカウンターストアを実装する:
- カウント状態
- doubleCount ゲッター
- 増分/減分/リセット操作
- 3つのコンポーネントで共有されている
(2) 上級問題(難易度:⭐⭐)
ショッピングカート(ストア)の実装:
- items配列
- itemCount / totalPrice のゲッター
- addItem / removeItem / clearCart アクション
- localStorage に保存する
- 5つのコンポーネント(ProductCard / CartIcon / CartPage / Checkout / Header)でテスト済み
(3) チャレンジ問題(難易度:⭐⭐⭐)
完全なeコマースのバックエンド「ストア」システムを実装する:
- 5つのストア:auth / cart / theme / notification / product
- 各ストアの「Full」状態 + ゲッター + アクション
- 永続化(auth/cart/theme)
- セットアップのスタイル + TypeScript
- storeToRefsの分解
- ストア間での呼び出し(カートが認証機能に呼び出しを行い、ログイン状態を確認する)
- 単体テスト(Vitest を使用)



