mcpField
V
vue-pinia

vue-pinia

github.com/mindrally開発28766 回インストール3 週間前に更新
リポジトリ

vue-pinia について

Vue 3でPiniaを使うための開発スキルです。共有クライアント状態をsetup storeに整理し、コンポーネント内のUI状態やサーバーデータと区別する方法を扱います。

主な機能

  1. 01Composition APIのsetup store
  2. 02共有状態、getter、action
  3. 03storeToRefsによるリアクティブな参照
  4. 04SSRを考慮したstoreの利用
  5. 05永続化とテストの指針

活用シーン

  1. 01ルートやコンポーネント間での状態共有
  2. 02store actionによる業務フローのモデル化
  3. 03storeの動作を個別にテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mindrally/skills --skill vue-pinia

インストール後、Claude Code で/vue-piniaを入力して読み込まれたか確認します。

詳細

Vue + Pinia

Pinia は Vue 3 の標準的な状態管理ライブラリです。Composition API で作成するセットアップストアを使うと、アプリケーション間で共有する状態を、ローカルの ref/computed 状態と同じように扱えます。

Pinia ストアを追加するワークフロー

  1. Pinia が適切なツールか判断する — 状態がルートやコンポーネント間で共有されるものであり、サーバーキャッシュデータでも純粋なローカル UI 状態でもないことを確認します(「状態の所有者」を参照)。
  2. ストアファイルを作成する — stores/ の下にドメインごとのファイルを 1 つ追加します。例:stores/cart.ts。defineStore('cart', () => { ... }) を使用します。
  3. 状態、ゲッター、アクションを定義する — ref/reactive で状態を宣言し、computed で値を導出し、すべての書き込みや副作用を関数(アクション)にまとめます。
  4. セットアップ関数からすべてを返す — 状態、ゲッター、アクションをすべて返し、Pinia devtools、SSR、プラグインから追跡できるようにします。
  5. コンポーネントで使用する — <script setup> の冒頭でストアを呼び出し、リアクティブな状態は storeToRefs() で分割代入し、アクションは直接分割代入します。
  6. SSR/ルーターガードでの使用に対応する — setup、ゲッター、アクションの内部でストアを呼び出すか、setup のコンテキスト外ではアクティブな Pinia インスタンスを明示的に渡します。
  7. 永続化とテストを追加する — 永続化するフィールドを許可リストで指定し、再ハイドレーションされたデータを検証し、@pinia/testing でアクションを直接テストします。

状態の所有者

  • コンポーネントだけで使用する状態は、ref、reactive、または computed を使ってコンポーネント内に保持します。
  • ルート、レイアウト、または関係のないコンポーネントツリーにまたがって共有されるクライアント状態には Pinia を使用します。
  • 共有可能なナビゲーション状態にはルートパラメーターとクエリー文字列を使用し、ストアに重複して保持しないでください。
  • サーバー状態には Nuxt の useFetch/useAsyncData、Vue 用 TanStack Query、または既存の API レイヤーを使用します。Pinia はクライアント状態用であり、データ取得キャッシュ用ではありません。
  • サーバーキャッシュデータを Pinia にコピーしないでください。ただし、独自のライフサイクルを持つ、意図的に編集可能なドラフト、オフラインキャッシュ、またはワークフローのスナップショットである場合を除きます。

ストア構造

  • Vue 3 Composition API のプロジェクトでは、旧式の Options API による defineStore({ state, getters, actions }) 形式よりも、defineStore('name', () => { ... }) を使ったセットアップストアを優先します。
  • stores/ の下に、stores/cart.ts や stores/session.ts のようなドメインごとのストアファイルを 1 つずつ作成します。
  • 状態、computed ゲッター、アクションは、まとまりのある 1 つのドメインを表す場合は一緒に管理します。
  • セットアップストアでは状態のプロパティをすべて返し、Pinia が devtools、SSR、プラグイン向けに追跡できるようにします。返されない ref はシステムのほかの部分から見えません。
  • ゲッターは純粋で副作用のないものにします。書き込み、I/O、オーケストレーションはアクションにまとめます。
// stores/cart.ts
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'

interface CartLine {
  id: string
  name: string
  quantity: number
  unitPrice: number
}

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

  const itemCount = computed(() =>
    lines.value.reduce((total, line) => total + line.quantity, 0),
  )

  const subtotal = computed(() =>
    lines.value.reduce((total, line) => total + line.quantity * line.unitPrice, 0),
  )

  function addLine(line: CartLine) {
    const existing = lines.value.find((item) => item.id === line.id)
    if (existing) {
      existing.quantity += line.quantity
      return
    }
    lines.value.push(line)
  }

  function removeLine(id: string) {
    lines.value = lines.value.filter((line) => line.id !== id)
  }

  function clearCart() {
    lines.value = []
  }

  return {
    lines,
    itemCount,
    subtotal,
    addLine,
    removeLine,
    clearCart,
  }
})

コンポーネントでの使用

  • ストアは <script setup> の冒頭、または setup 関数、ゲッター、アクションの内部で呼び出します。条件付きで呼び出したり、インポート時に読み込まれる通常の .js/.ts モジュール内で呼び出したりしないでください。
  • コンポーネントでストアの状態やゲッターを分割代入する場合は、リアクティビティを維持できるよう storeToRefs() を使用します。
  • 必要に応じてアクションを直接分割代入します。アクションはストアにバインドされているため、storeToRefs() は不要です。
  • 大規模なビジネスワークフローをコンポーネントに記述しないでください。テストや再利用ができるよう、ストアのアクションまたはコンポーザブルに移します。
  • 状態の導出にはウォッチャーより computed 値を優先します。値を再計算するだけの watch は、通常 computed に置き換えられます。
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCartStore } from '@/stores/cart'

const cart = useCartStore()
const { itemCount, subtotal } = storeToRefs(cart)
const { clearCart } = cart
</script>

<template>
  <div>
    <p>{{ itemCount }} items — ${{ subtotal.toFixed(2) }}</p>
    <button type="button" :disabled="itemCount === 0" @click="clearCart">
      Clear cart
    </button>
  </div>
</template>

TypeScript

  • ストアの状態、アクションのペイロード、API レスポンスには明示的な型を付けます。
  • any は避けます。unknown を使い、外部入力(API レスポンス、永続化されたストレージ)を絞り込んでから状態に反映します。
  • コンポーネント間または API 境界で共有されるオブジェクト状態には、インターフェースを使用します。
  • ワークフローのステータスやエラー状態には、判別可能なユニオンを優先します。例:{ status: 'idle' } | { status: 'loading' } | { status: 'error'; error: string }。
  • ストア ID(defineStore の最初の引数)は、devtools や永続化キーに表示されるため、安定した説明的な値にします。

アクションと副作用

  • アクションは同期または非同期にできます。各アクションは、ユーザーまたはドメインのワークフローを1つだけ扱うようにしてください。
  • ストアの状態を変更する前に、アクションの入力を境界で検証してください。
  • UIが非同期ワークフローに依存する場合は、明示的な status、error、lastUpdatedAt フィールドで状態を表してください。
  • 非同期アクションを再試行する前に古いエラーをリセットし、以前の失敗がUIに残らないようにしてください。
  • サブスクリプション、インターバル、ソケット、ブラウザーのリスナーは、ストアがそのライフサイクルとクリーンアップを管理する場合(明示的な connect()/disconnect() アクションのペアなど)を除き、ストアの外に置いてください。

SSR、Nuxt、ルーターガード

  • SSRのコンテキストでは、Piniaがアクティブなアプリインスタンスを解決できるよう、setup、ゲッター、またはアクション内でストアを使用してください。
  • ルーターガードなど、setupの外でストアを使用する場合、フレームワークが必要とするならアクティブなPiniaインスタンスを渡してください(Nuxtの自動インポートでは自動的に処理されます)。
  • サーバーレンダリング中にブラウザー専用のストレージ(localStorage、window)を読み込まないでください。import.meta.client(Nuxt)または onMounted によるチェックで保護してください。
  • Nuxtでは、Piniaインスタンスを手動で配線するのではなく、@pinia/nuxt 統合とSSR対応のコンポーザブルをデータ取得に使用してください。
  • モジュールレベルのシングルトンではなく、リクエストごとにフレームワークが作成するPiniaインスタンスを使用し、リクエスト間でシングルトンの状態が漏れるのを防いでください。
// router guard example (outside setup)
router.beforeEach((to) => {
  const auth = useAuthStore(pinia) // pass the active Pinia instance explicitly
  if (to.meta.requiresAuth && !auth.isAuthenticated) {
    return { name: 'login' }
  }
})

永続化

  • 設定や未完了のローカル下書きなど、再読み込み後も保持する必要があるフィールドだけを永続化してください。
  • シークレット、アクセストークン、リフレッシュトークン、生のPII、認可判断をブラウザーストレージに永続化しないでください。
  • 永続化されたストアのスキーマにはフィールドの許可リストとバージョン管理されたマイグレーションを使用し、古い形式のデータで新しいバージョンのストアがクラッシュしないようにしてください。
  • 永続化された状態は信頼できない入力として扱い、重要なワークフローで使用する前に検証してください。ユーザーは localStorage を直接編集できます。
  • 永続化された値に依存するUIをレンダリングする前に、ハイドレーションのタイミングを考慮してください。特にSSRでは、サーバーはブラウザーストレージにアクセスできません。

テストとツール

  • ストアを必要とするコンポーネントテストには @pinia/testing(createTestingPinia())を使用し、アクションはデフォルトでスタブ化してください。
  • コンポーネントに依存せず、ドメインの振る舞いやエッジケースを検証するには、ストアのアクションを直接テストしてください。
  • テストケース間で状態が共有されないよう、テストごとにPiniaをリセットしてください。
  • プロジェクトでViteのHMRパターンを使用している場合は、ストアに acceptHMRUpdate() を追加してHMRをサポートしてください。
  • 明確な状態名を使い、単一のドメインに焦点を当てたストアにして、Vue Devtoolsでストアを調べやすくしてください。
import { setActivePinia, createPinia } from 'pinia'
import { describe, it, expect, beforeEach } from 'vitest'
import { useCartStore } from '@/stores/cart'

describe('cart store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  it('merges quantities for duplicate line items', () => {
    const cart = useCartStore()
    cart.addLine({ id: '1', name: 'Widget', quantity: 1, unitPrice: 10 })
    cart.addLine({ id: '1', name: 'Widget', quantity: 2, unitPrice: 10 })
    expect(cart.itemCount).toBe(3)
  })
})

アンチパターン

  • すべてのリアクティブな値をPiniaに詰め込まないでください。ほとんどのコンポーネント状態はローカルに保つべきです。
  • storeToRefs() を使わずにストアから状態を直接分割代入しないでください。リアクティビティが気づかないうちに失われます。
  • ストアのアクションを通じてpropsやルートオブジェクトを変更しないでください。
  • サーバー専用オブジェクト、リクエストインスタンス、DOMノード、タイマーをストアの状態に入れないでください。これらはシリアライズできず、SSRやDevtoolsで問題を引き起こします。
  • setup関数内でストア間の循環参照を作らないでください。モジュールスコープで2つのストアが互いを読み取るのではなく、アクションや算出値を通じてストアを組み合わせてください。

コメント