Vue 3 で Pinia を活用した状態管理の実践

Pinia とは

Pinia は Vue 3 向けに設計された現代的な状態管理ライブラリです。Vuex の後継として開発され、Vue 公式が推奨するデファクトスタンダードとなっています。シンプルな API、優れた TypeScript サポート、軽量な体格が特徴です。

セットアップとインストール

Pinia をプロジェクトに追加するには、以下のいずれかのコマンドを実行します:


npm install pinia
# または
yarn add pinia

基本的な利用手順

1. ストアの定義

各機能をモジュール単位で明確に分離します。

// stores/counter.ts
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    value: 0,
    label: 'default'
  }),
  getters: {
    tripleValue: (state) => state.value * 3,
    formattedValue: (state) => (prefix: string) => {`${prefix}: ${state.value}`}
  },
  actions: {
    increase(delta: number = 1) {
      this.value += delta
    },
    async refreshValue() {
      const res = await fetch('/api/latest')
      const data = await res.json()
      this.value = data.current
    }
  }
})

2. アプリケーションへの統合

Vue アプリケーションの初期化時に Pinia をプラグインとして登録します。

// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

3. コンポーネントでの利用

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'

const store = useCounterStore()
const onAdd = () => store.increase(2)
const onUpdateLabel = () => {
  store.$patch({ label: 'updated' })
}
</script>

<template>
  <div class="counter">
    <p>値:{{ store.value }}(3倍:{{ store.tripleValue }})</p>
    <p>ラベル:{{ store.formattedValue('Number') }}</p>
    <button @click="onAdd">+2 増加</button>
    <button @click="onUpdateLabel">ラベル更新</button>
  </div>
</template>

主要な概念

state:基本的な状态保持

初期状態を関数で返す形式で定義します。配列やオブジェクトなども реактивに扱えます。

$patch メソッドで複数の状態を一括更新可能です:

store.$patch({
  value: 10,
  label: ' Immediate update'
})

getters:依存計算ロジック

従来の computed に相当します。返り値として関数を持つことも可能で、パラメータを受け取る柔軟な計算が実現できます:

getters: {
  withThreshold: (state) => (threshold: number) => state.value >= threshold
}

actions:状態変更ロジック

>

非同期処理(例:API 呼び出し、ロギング、デバイス操作など)を含めることも自然に記述可能です:

actions: {
  async loadConfig() {
    try {
      const cfg = await api.getConfig()
      this.$patch({ config: cfg })
    } catch (e) {
      console.error('設定読み込み失敗:', e)
    }
  }
}

高度な活用方法

プラグイン:拡張機能の追加

Pinia のグローバルな挙動をカスタマイズするための仕組みです。たとえば、状態変更時のログ記録や永続化ミドルウェアの統合が可能です:

import { persistancePlugin } from '@/plugins/persistence'

const pinia = createPinia()
pinia.use(persistancePlugin)

// プラグイン実装例
export const persistancePlugin = (context) => {
  const { store } = context
  store.$subscribe((event, state) => {
    localStorage.setItem('pinia-' + store.$id, JSON.stringify(state))
  })
}

SSR での利用

Pinia は Vue 3 の SSR 設定と自然に連携します。クライアントとサーバー間での状態同期を実現するには、`createPinia()` をリクエストごとに生成し、グローバルなインスタンスを避けることが推奨されます。

// server-entry.ts
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'

export function createApp() {
  const app = createSSRApp(App)
  const pinia = createPinia()
  app.use(pinia)
  return { app, pinia }
}

Vuex との比較 : Pinia の利点

  • 型推論が自然に機能し、型安全な書きやすさ向上
  • エントリーポイントが単一の関数で統一(`defineStore`)
  • `mapState` / `mapActions` といった補助関数が不要(コンポジション API 風)
  • ネスト構造を持つモジュールが明示的に不要(モジュール分割无需)

動的状態の追加とその制約

動的状態追加の方法

通常、Pinia では初期状態で定義された属性のみが型として保証されます。ただし、必要に応じて以下のような手段で動的追加が可能です:

① $patch を使った安全な追加

const store = useAppStore()
store.$patch({ sessionData: { token: 'xxx', expires: Date.now() } })

② $state による全体置換

store.$state = {
  ...store.$state,
  sessionData: { token: 'xxx', expires: Date.now() }
}

③ 直接代入(非推奨)

phản응性が失われる可能性があるため、避けるべきです。

実用的な動的状態管理パターン

型安全かつ柔軟性を担保したい場合、以下のような専用オブジェクトフィールドを用意し、操作専用メソッドを提供するのが望ましいです:

interface DynamicStateMap {
  [key: string]: any
}

export const useDynamicStore = defineStore('dynamic', {
  state: () => ({
    dynamicEntries: {} as DynamicStateMap
  }),
  actions: {
    registerEntry(key: string, value: any) {
      this.dynamicEntries = {
        ...this.dynamicEntries,
        [key]: value
      }
    },
    removeEntry(key: string) {
      const { [key]: removed, ...rest } = this.dynamicEntries
      this.dynamicEntries = rest
    }
  }
})

タグ: Vite composition-api Pinia TypeScript SSR

8月17日 02:02 投稿