404 Not Found

404 Not Found


nginx

Vue 3 Pinia の状態管理:defineStore、ゲッター、アクション、および永続化

Piniaは、Vue 3が推奨する公式の状態管理ライブラリであり、Vuexに代わるものです。Piniaは、より簡潔なAPI、TypeScriptの完全なサポート、モジュール型のホットリロード、およびネイティブなDevToolsサポートを提供します。VueチームはPiniaをデフォルトの推奨ライブラリとしました(Vuexはもはやメンテナンスされていません)。

Pinia を使えば、provide/inject よりも体系的な方法で コンポーネント間で共有される状態(ユーザー情報、ショッピングカート、グローバル設定など)を管理でき、Vuex よりも 50% 簡潔に記述できます。

1. 学習内容


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 を使用した後:


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は、コンポーネントレベルで再利用可能なロジック(useMouseuseFetchなど)に使用します。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との統合機能が組み込まれています。

📖 まとめ


📝 練習問題

(1) 基礎問題(難易度:⭐)

簡単なカウンターストアを実装する:

(2) 上級問題(難易度:⭐⭐)

ショッピングカート(ストア)の実装:

(3) チャレンジ問題(難易度:⭐⭐⭐)

完全なeコマースのバックエンド「ストア」システムを実装する:

  1. 5つのストア:auth / cart / theme / notification / product
  2. 各ストアの「Full」状態 + ゲッター + アクション
  3. 永続化(auth/cart/theme)
  4. セットアップのスタイル + TypeScript
  5. storeToRefsの分解
  6. ストア間での呼び出し(カートが認証機能に呼び出しを行い、ログイン状態を確認する)
  7. 単体テスト(Vitest を使用)
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%