pinia について
JARVIS AI アシスタントを開発する方向けの Pinia 状態管理スキルです。型付きストア、SSR の分離、安全な永続化、効率的なリアクティブ更新を扱います。
主な機能
- 01TypeScript による Setup Store
- 02SSR リクエスト間の状態分離
- 03機密データの永続化防止
- 04ストアアクションの入力検証
- 05算出値と選択的サブスクリプション
- 06Vitest によるストアテスト
活用シーン
- 01システムメトリクスと状態の追跡
- 02設定と HUD 構成の管理
- 03コマンド履歴とキューの保持
- 04アプリケーションのリアルタイムデータ同期
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ martinholovsky/ claude-skills-generator --skill piniaインストール後、Claude Code で/piniaを入力して読み込まれたか確認します。
同梱ファイルreferences/advanced-patterns.mdreferences/security-examples.md
詳細
Pinia 状態管理スキル
ファイル構成: このスキルは分割構成を使用します。高度なパターンやセキュリティの例については、
references/を参照してください。
1. 概要
このスキルでは、JARVIS AI アシスタントでアプリケーションの状態を管理するための Pinia の知識を提供します。対象には、システムメトリクス、ユーザー設定、HUD 構成が含まれます。
リスクレベル: MEDIUM - 機密性の高い状態、SSR に関する考慮事項、データ漏えいの可能性を管理します
主なユースケース:
- システムメトリクスとステータスの追跡
- ユーザー設定と環境設定
- HUD 構成の状態
- コマンド履歴とキュー
- リアルタイムのデータ同期
2. 主な責務
2.1 基本原則
- TDD を優先: 実装の前にストアのテストを作成します
- パフォーマンスを考慮: サブスクリプションと算出値を最適化します
- 型安全性: TypeScript の完全な型付けでストアを定義します
- SSR のセキュリティ: リクエスト間での状態漏えいを防ぎます
- Composition API: TypeScript のサポートを向上させるため、セットアップストアを使用します
- 状態の最小化: 必要なデータのみを保存し、残りは導出します
- アクションの検証: 変更前にアクション内で入力を検証します
- 永続化のセキュリティ: 機密データを localStorage に永続化しないでください
3. 技術スタックとバージョン
3.1 推奨バージョン
| パッケージ | バージョン | 備考 |
|---|---|---|
| pinia | ^2.1.0 | 最新の安定版 |
| @pinia/nuxt | ^0.5.0 | Nuxt との統合 |
| pinia-plugin-persistedstate | ^3.0.0 | 永続化(任意) |
3.2 Nuxt の設定
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
pinia: {
storesDirs: ['./stores/**']
}
})
3.3 実装ワークフロー(TDD)
すべてのストアで、次のワークフローに従ってください。
ステップ 1: 失敗するテストを先に作成
// tests/stores/metrics.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useMetricsStore } from '~/stores/metrics'
describe('MetricsStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should initialize with default values', () => {
const store = useMetricsStore()
expect(store.cpu).toBe(0)
expect(store.memory).toBe(0)
})
it('should clamp values within valid range', () => {
const store = useMetricsStore()
store.updateCpu(150)
expect(store.cpu).toBe(100)
store.updateCpu(-50)
expect(store.cpu).toBe(0)
})
it('should compute health status correctly', () => {
const store = useMetricsStore()
store.updateCpu(95)
store.updateMemory(90)
expect(store.healthStatus).toBe('critical')
})
})
ステップ 2: テストに合格するための最小限の実装
// stores/metrics.ts
export const useMetricsStore = defineStore('metrics', () => {
const cpu = ref(0)
const memory = ref(0)
const healthStatus = computed(() => {
const avg = (cpu.value + memory.value) / 2
if (avg > 90) return 'critical'
if (avg > 70) return 'warning'
return 'healthy'
})
function updateCpu(value: number) {
cpu.value = Math.max(0, Math.min(100, value))
}
function updateMemory(value: number) {
memory.value = Math.max(0, Math.min(100, value))
}
return { cpu, memory, healthStatus, updateCpu, updateMemory }
})
ステップ 3: パターンに従ってリファクタリング
- 検証ロジックを切り出します
- TypeScript のインターフェースを追加します
- 算出値の依存関係を最適化します
ステップ 4: 全体の検証を実行
npm run test -- --filter=stores
npm run typecheck
npm run build
4. 実装パターン
4.1 TypeScript を使用したセットアップストア
// stores/jarvis.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface SystemMetrics {
cpu: number
memory: number
network: number
timestamp: number
}
interface JARVISState {
status: 'idle' | 'listening' | 'processing' | 'responding'
securityLevel: 'normal' | 'elevated' | 'lockdown'
}
export const useJarvisStore = defineStore('jarvis', () => {
// State
const state = ref<JARVISState>({
status: 'idle',
securityLevel: 'normal'
})
const metrics = ref<SystemMetrics>({
cpu: 0,
memory: 0,
network: 0,
timestamp: Date.now()
})
// Getters
const isActive = computed(() =>
state.value.status !== 'idle'
)
const systemHealth = computed(() => {
const avg = (metrics.value.cpu + metrics.value.memory) / 2
if (avg > 90) return 'critical'
if (avg > 70) return 'warning'
return 'healthy'
})
// Actions
function updateMetrics(newMetrics: Partial<SystemMetrics>) {
// ✅ Validate input
if (newMetrics.cpu !== undefined) {
metrics.value.cpu = Math.max(0, Math.min(100, newMetrics.cpu))
}
if (newMetrics.memory !== undefined) {
metrics.value.memory = Math.max(0, Math.min(100, newMetrics.memory))
}
if (newMetrics.network !== undefined) {
metrics.value.network = Math.max(0, newMetrics.network)
}
metrics.value.timestamp = Date.now()
}
function setStatus(newStatus: JARVISState['status']) {
state.value.status = newStatus
}
function setSecurityLevel(level: JARVISState['securityLevel']) {
state.value.securityLevel = level
// ✅ Audit security changes
console.info(`Security level changed to: ${level}`)
}
return {
state,
metrics,
isActive,
systemHealth,
updateMetrics,
setStatus,
setSecurityLevel
}
})
4.2 ユーザー設定ストア(永続化あり)
// stores/preferences.ts
export const usePreferencesStore = defineStore('preferences', () => {
const preferences = ref({
theme: 'dark' as 'dark' | 'light',
hudOpacity: 0.8,
soundEnabled: true
})
function updatePreference<K extends keyof typeof preferences.value>(
key: K, value: typeof preferences.value[K]
) {
if (key === 'hudOpacity' && (value < 0 || value > 1)) return
preferences.value[key] = value
}
return { preferences, updatePreference }
}, {
persist: {
key: 'jarvis-preferences',
paths: ['preferences.theme', 'preferences.hudOpacity']
// ❌ Never persist: tokens, passwords, API keys
}
})
4.3 コマンドキューストア
// stores/commands.ts
interface Command {
id: string
action: string
status: 'pending' | 'executing' | 'completed' | 'failed'
}
export const useCommandStore = defineStore('commands', () => {
const queue = ref<Command[]>([])
const history = ref<Command[]>([])
const MAX_HISTORY = 100
const pendingCommands = computed(() =>
queue.value.filter(cmd => cmd.status === 'pending')
)
function addCommand(action: string) {
const cmd: Command = { id: crypto.randomUUID(), action, status: 'pending' }
queue.value.push(cmd)
return cmd.id
}
function completeCommand(id: string, status: 'completed' | 'failed') {
const idx = queue.value.findIndex(cmd => cmd.id === id)
if (idx !== -1) {
const [cmd] = queue.value.splice(idx, 1)
cmd.status = status
history.value = [cmd, ...history.value].slice(0, MAX_HISTORY)
}
}
return { queue, history, pendingCommands, addCommand, completeCommand }
})
4.4 SSR に安全なストアの使用
<script setup lang="ts">
// ✅ Safe for SSR - store initialized per-request
const jarvisStore = useJarvisStore()
// ✅ Fetch data on server
const { data } = await useFetch('/api/metrics')
// Update store with fetched data
if (data.value) {
jarvisStore.updateMetrics(data.value)
}
</script>
4.5 ストアの合成
// stores/dashboard.ts
export const useDashboardStore = defineStore('dashboard', () => {
// ✅ Compose from other stores
const jarvisStore = useJarvisStore()
const commandStore = useCommandStore()
const dashboardStatus = computed(() => ({
systemHealth: jarvisStore.systemHealth,
pendingCommands: commandStore.pendingCommands.length,
isActive: jarvisStore.isActive
}))
return {
dashboardStatus
}
})
5. セキュリティ基準
5.1 OWASP 対応
| OWASP カテゴリー | リスク | 緩和策 |
|---|---|---|
| A01 アクセス制御の不備 | MEDIUM | アクションを検証し、権限を確認します |
| A04 安全でない設計 | MEDIUM | SSR の状態を分離します |
| A07 認証の失敗 | MEDIUM | トークンを永続化しないでください |
5.3 機密データの取り扱い
// ❌ NEVER persist: tokens, API keys, passwords
// ✅ Store sensitive data in memory only (no persist option)
const authStore = defineStore('auth', () => {
const token = ref<string | null>(null)
return { token }
})
5.5 パフォーマンスパターン
パターン 1: 選択的なサブスクリプション
// BAD - Subscribes to entire store
const store = useJarvisStore()
watch(() => store.state, () => { /* ... */ }, { deep: true })
// GOOD - Subscribe to specific properties
const store = useJarvisStore()
watch(() => store.state.status, (newStatus) => {
console.log('Status changed:', newStatus)
})
パターン 2: 算出ゲッター(メモ化)
// BAD - Recalculates on every access
function getFilteredItems() {
return items.value.filter(i => i.active)
}
// GOOD - Cached until dependencies change
const filteredItems = computed(() =>
items.value.filter(i => i.active)
)
パターン 3: 更新の一括処理
// BAD - Multiple reactive triggers
function updateAll(data: MetricsData) {
metrics.value.cpu = data.cpu
metrics.value.memory = data.memory
metrics.value.network = data.network
}
// GOOD - Single reactive trigger
function updateAll(data: MetricsData) {
metrics.value = { ...metrics.value, ...data, timestamp: Date.now() }
}
パターン 4: ストアの遅延初期化
// BAD - Store initializes immediately
const heavyStore = useHeavyDataStore()
// GOOD - Initialize only when needed
const heavyStore = ref<ReturnType<typeof useHeavyDataStore> | null>(null)
function loadHeavyData() {
if (!heavyStore.value) {
heavyStore.value = useHeavyDataStore()
}
return heavyStore.value
}
パターン 5: 楽観的更新
// BAD - Wait for server response
async function deleteItem(id: string) {
await api.delete(`/items/${id}`)
items.value = items.value.filter(i => i.id !== id)
}
// GOOD - Update immediately, rollback on error
async function deleteItem(id: string) {
const backup = [...items.value]
items.value = items.value.filter(i => i.id !== id)
try {
await api.delete(`/items/${id}`)
} catch (error) {
items.value = backup // Rollback
throw error
}
}
6. テストと品質
vitest の例を含む TDD ワークフローの全容については、セクション 3.3 を参照してください。
8. よくあるアンチパターン
セキュリティ上のアンチパターン
// ❌ Global state leaks between SSR users
const state = reactive({ user: null })
// ✅ Pinia isolates per-request
export const useUserStore = defineStore('user', () => {
const user = ref(null)
return { user }
})
// ❌ Never persist auth tokens (XSS risk)
persist: { paths: ['authToken'] }
// ✅ Use httpOnly cookies for auth
パフォーマンス上のアンチパターン
Good/Bad の例を含む詳細なパフォーマンスパターンについては、セクション 5.5 を参照してください。
13. 実装前チェックリスト
フェーズ 1: コードを書く前
- TypeScript の型を使用してストアのインターフェースを設計済み
- 失敗するテストを含むテストファイルを作成済み
- セキュリティ要件(永続化、SSR)を特定済み
- ユースケースに適したパフォーマンスパターンを選定済み
フェーズ 2: 実装中
- 機能を追加するたびにテストが合格
- すべての入力をアクションで検証
- 算出値の依存関係を最小限に抑制
- 永続化する状態に機密データがない
- SSR の状態を適切に分離
フェーズ 3: コミット前
- すべてのストアテストが合格:
npm run test -- --filter=stores - 型チェックが合格:
npm run typecheck - ビルドが成功:
npm run build - Pinia の外部にグローバル状態がない
- 状態の形状を型で文書化済み
14. まとめ
Pinia は JARVIS に型安全な状態管理を提供します。
- TDD を最優先:実装の前にストアのテストを作成します
- パフォーマンス:サブスクリプションと算出値を最適化します
- セキュリティ:機密データは決して永続化せず、SSR の状態を分離します
- 型安全性:TypeScript を全面的に活用できるセットアップストアを使用します
参考資料:高度なパターンとセキュリティの例については、references/ を参照してください。
コメント
Markdown 対応