はい、承知いたしました。Vueの新しい状態管理ライブラリ「Pinia」について、初心者向けに使い方を徹底解説する約5000語の詳細な記事を作成します。
Vueの新しい常識!Pinia入門【初心者向けに使い方を徹底解説】
1. はじめに:Vueの状態管理のこれまでとこれから
Vue.jsでアプリケーションを開発していると、必ずと言っていいほど「状態管理」という壁に直面します。コンポーネントが小さく、数が少ないうちは問題ありません。親から子へはpropsで、子から親へはemitでデータを渡せば事足ります。しかし、アプリケーションが大規模化し、コンポーネントの階層が深くなったり、兄弟関係にあるコンポーネント間でデータを共有したくなったりすると、この「バケツリレー」方式は途端に複雑化し、コードの見通しを著しく悪化させます。
この問題を解決するために登場したのが、状態管理ライブラリです。Vueの世界では、長らくVuexがその役割を担ってきました。Vuexは、アプリケーションの全てのコンポーネントのための「中央集権的なストア(倉庫)」を提供し、状態を一元管理することで、予測可能でメンテナンスしやすいアプリケーション開発を可能にしてきました。
しかし、Vue 3とComposition APIの登場により、Vueのエコシステムは大きな変革期を迎えました。より柔軟で、型安全(TypeScriptとの親和性が高い)な開発スタイルが主流となる中で、Vuexの持ついくつかの概念(mutationsの強制、冗長なモジュール定義など)が、新しい時代の開発スタイルと少しずつそぐわなくなってきました。
そこに彗星の如く現れたのが、今回徹底解説するPinia(ピニア)です。
Piniaは、Vuexの次世代版としてVueコアチームによって開発され、現在ではVueの公式状態管理ライブラリとして推奨されています。シンプルさ、直感的なAPI、そしてTypeScriptとの完璧な親和性を武器に、Vue開発者たちの間で急速に普及し、今や「Vueの新しい常識」となりつつあります。
この記事では、Vuexしか知らない方、状態管理ライブラリ自体が初めてという初心者の方を対象に、Piniaの魅力を余すところなくお伝えします。
- Piniaとは何か、Vuexと何が違うのか
- 環境構築から基本的な使い方(Storeの定義、State、Getters、Actions)
- より実践的な応用テクニック(非同期処理、Store間の連携、プラグイン)
- 簡単なTODOアプリケーションを作成する実践的なハンズオン
この記事を読み終える頃には、あなたはPiniaを自信を持って使いこなし、モダンでスケーラブルなVueアプリケーションを構築するための強力な武器を手に入れていることでしょう。さあ、Vue開発の新しい世界へ一緒に旅立ちましょう!
2. Piniaとは?Vuexとの違いを理解しよう
Piniaを学び始める前に、まず「Piniaがどのような思想で作られ、従来のVuexと何が違うのか」を理解することが重要です。この違いを把握することで、Piniaの学習がよりスムーズに進みます。
Piniaのコンセプト
Piniaの公式ドキュメントには、その核心的な思想が述べられています。要約すると、以下の3つのキーワードに集約されます。
- シンプル (Simple): Piniaは、覚えるべき概念が非常に少ないのが特徴です。Vuexにあった
Mutationsは廃止され、State、Getters、Actionsという3つの主要な概念だけで構成されています。APIも直感的で、まるでコンポーネントのsetup関数を書くような感覚でストアを定義できます。 - 型安全 (Type-Safe): PiniaはTypeScriptを念頭に置いて設計されています。特別な設定をしなくても、完璧な型推論が働き、エディタの自動補完機能を最大限に活用できます。これにより、タイプミスによるバグを未然に防ぎ、開発体験を劇的に向上させます。
- 拡張性 (Extensible): Piniaは非常に軽量でありながら、プラグインシステムを通じて機能を拡張できます。また、Vue Devtoolsとの連携も完璧で、状態の変更履歴を追跡したり、タイムトラベルデバッグを行ったりすることが容易です。
Vuexとの主な違い
PiniaとVuexの具体的な違いを、比較表を交えながら見ていきましょう。
| 機能/概念 | Vuex (4.x) | Pinia | 特徴とメリット |
|---|---|---|---|
| 状態変更 | Mutations (同期的) & Actions (非同期可) |
Actions のみ |
Mutationsの廃止。PiniaではActionsが直接Stateを変更します。同期・非同期を区別する必要がなくなり、コードがシンプルになります。Devtoolsでの追跡も問題なく可能です。 |
| モジュール | 複雑なネスト構造、namespacedプロパティ |
ストアをimportして使うだけ | モジュール管理の簡素化。Piniaには厳密な意味での「モジュール」はありません。各ストアが独立したモジュールのように振る舞い、必要なコンポーネントでimportして使うだけです。名前空間の衝突を心配する必要もありません。 |
| TypeScript | 型定義が冗長になりがち | 完璧な型推論 | 圧倒的な型サポート。Piniaは型定義のボイラープレート(お決まりのコード)がほとんど不要です。defineStoreで定義するだけで、State、Getters、Actionsの型が自動的に推論されます。 |
| APIスタイル | Options APIライク | Composition APIライク | Composition APIとの親和性。Piniaのストア定義(特にSetup Store形式)は、ref, computed を使うため、Composition APIに慣れた開発者にとっては非常に直感的です。 |
| サイズ | 約3.8kB (gzipped) | 約1.6kB (gzipped) | 軽量。PiniaはVuexよりもバンドルサイズが小さく、アプリケーションのパフォーマンス向上に貢献します。 |
| Storeアクセス | this.$store (Options API) useStore() (Composition API) |
useMyStore() (Composition API) |
より明確なアクセス。Piniaでは各ストア専用のuse関数(例: useCounterStore)を呼び出すため、どのストアにアクセスしているかが一目瞭然です。 |
Mutationsの廃止は、Piniaの最も大きな変更点の一つです。Vuexでは「Stateの変更は必ずMutationを通じて行う」という厳格なルールがありました。これは、状態の変更履歴を追跡しやすくするための設計でしたが、非同期処理のためにActionからMutationをcommitするという一手間が必要でした。Piniaではこの制約をなくし、Actionから直接Stateを書き換えられるようにしました。これにより、コード量が減り、思考のフローがより自然になります。「Devtoolsでの追跡はどうなるの?」と心配になるかもしれませんが、心配は無用です。Piniaは内部的に変更をラップしているため、Vue DevtoolsでActionによるStateの変更を正確に追跡できます。
モジュール管理の簡素化も大きな魅力です。Vuexでは、ストアが肥大化するとモジュールに分割しますが、ネストしたモジュールへのアクセスパスが長くなったり(dispatch('cart/addItem', ...))、namespacedを意識したりする必要がありました。Piniaでは、ストアを機能ごとにファイルに分けて定義し、必要な場所でimport { useTodoStore } from '@/stores/todo'のようにインポートするだけです。これにより、ストア同士が疎結合になり、再利用性やテストのしやすさが向上します。
なぜ今、Piniaを選ぶべきか?
結論として、Piniaは現代のVue開発において最適な選択肢と言えます。
- 学習コストが低い: 覚えることが少なく、Vueの基本的な知識があればすぐに使い始められます。
- Composition APIと相性抜群:
setup構文に慣れているなら、違和感なくストアを構築できます。 - TypeScriptの恩恵を最大化できる: 型の安全性により、大規模なアプリケーションでも安心して開発を進められます。
- 公式の推奨: Vueの生みの親であるEvan You氏を含むコアチームが公式ライブラリとして推奨しており、将来性も安泰です。
これからVueで状態管理を学ぶなら、迷わずPiniaから始めることを強くお勧めします。Vuexでの開発経験がある方も、Piniaのシンプルさと開発体験の良さにきっと驚くはずです。
3. Piniaを使ってみよう!環境構築から基本のCRUDまで
理論を学んだところで、いよいよ実践です。ここでは、VueプロジェクトにPiniaを導入し、基本的なストアを作成・利用する方法をステップバイステップで解説します。カウンターアプリケーションを例に、State(状態)、Getters(計算値)、Actions(操作)というPiniaの3大要素をマスターしましょう。
3-1. プロジェクトの準備
Piniaを始める最も簡単な方法は、プロジェクト作成時にPiniaを導入することです。
新規プロジェクトにPiniaを導入する場合
ターミナルで以下のコマンドを実行します。これはVueの公式プロジェクト作成ツールです。
bash
npm create vue@latest
コマンドを実行すると、プロジェクト名や各種設定を対話形式で質問されます。その中で「Add Pinia for state management?」と聞かれるので、「Yes」を選択してください。
“`
✔ Project name: … pinia-project
✔ Add TypeScript? … Yes
✔ Add JSX Support? … No
✔ Add Vue Router for Single Page Application development? … Yes
✔ Add Pinia for state management? … Yes <– ここでYesを選択
✔ Add Vitest for Unit Testing? … No
✔ Add an End-to-End Testing Solution? … No
✔ Add ESLint for code quality? … Yes
✔ Add Prettier for code formatting? … Yes
Scaffolding project in ./pinia-project…
Done.
“`
これにより、Piniaのインストールと初期設定が完了したVueプロジェクトが自動的に生成されます。
既存のプロジェクトにPiniaを追加する場合
すでに存在するVue 3プロジェクトにPiniaを追加することも簡単です。まず、Piniaをインストールします。
bash
npm install pinia
次に、アプリケーションのエントリーポイントである src/main.ts (または main.js) を編集して、PiniaをVueアプリケーションに登録します。
“`typescript
// src/main.ts
import { createApp } from ‘vue’
import { createPinia } from ‘pinia’ // Piniaをインポート
import App from ‘./App.vue’
const app = createApp(App)
app.use(createPinia()) // Piniaインスタンスを作成して登録
app.mount(‘#app’)
“`
createPinia() でPiniaのルートインスタンスを作成し、app.use() でそれをVueアプリケーション全体で使えるように登録します。たったこれだけで、プロジェクト内のどのコンポーネントからでもPiniaのストアにアクセスする準備が整いました。
3-2. Storeの定義:最初のストアを作ってみよう (defineStore)
ストアは、アプリケーションの状態を保持し、それを操作するためのロジックをカプセル化したものです。慣例として、ストアのファイルは src/stores ディレクトリに配置します。
まず、src/stores ディレクトリを作成し、その中に counter.ts というファイルを作成しましょう。
src/
├── stores/
│ └── counter.ts <-- 新しく作成
└── main.ts
...
counter.ts の中で、defineStore 関数を使って最初のストアを定義します。
“`typescript
// src/stores/counter.ts
import { defineStore } from ‘pinia’
// defineStore を使ってストアを定義
// 第一引数:ストアの一意なID (必須)
// 第二引数:オプションオブジェクト (state, getters, actions を含む)
export const useCounterStore = defineStore(‘counter’, {
// state: リアクティブなデータを定義する (Vueのdataに相当)
state: () => ({
count: 0,
userName: ‘John Doe’,
}),
// getters: stateから派生した値を計算する (Vueのcomputedに相当)
getters: {
// 基本的なgetter
doubleCount: (state) => state.count * 2,
// getter内で `this` を使って他のgetterにアクセスすることも可能
doubleCountPlusOne(): number {
return this.doubleCount + 1
},
// stateの値を元にメッセージを生成
displayMessage: (state) => {
return `現在のカウントは ${state.count}、ユーザー名は ${state.userName} です。`
}
},
// actions: stateを変更するメソッドを定義する (Vueのmethodsに相当)
actions: {
increment() {
// stateへのアクセスは this を使う
this.count++
},
decrement() {
this.count–
},
// 引数を受け取ることも可能
add(amount: number) {
this.count += amount
},
// 他のactionを呼び出すことも可能
incrementAndAdd(amount: number) {
this.increment()
this.add(amount)
},
// stateを直接書き換えるだけでなく、パッチでまとめて更新もできる
reset() {
this.$patch({
count: 0
})
}
},
})
“`
コードを詳しく見ていきましょう。
defineStore(id, options):- 第一引数の
'counter'は、このストアを識別するための一意なIDです。このIDはDevtoolsなどで使われるため、必須です。 - 第二引数は、ストアの構成を定義するオブジェクトです。
- 第一引数の
state:- ストアが持つべきリアクティブなデータを定義する関数です。必ずアロー関数で返す必要があります。これは、サーバーサイドレンダリング(SSR)環境での状態の共有を防ぎ、各インスタンスが独立した状態を持つようにするためです。Vueコンポーネントの
dataが関数であるのと同じ理由です。
- ストアが持つべきリアクティブなデータを定義する関数です。必ずアロー関数で返す必要があります。これは、サーバーサイドレンダリング(SSR)環境での状態の共有を防ぎ、各インスタンスが独立した状態を持つようにするためです。Vueコンポーネントの
getters:stateや他のgetterの値に基づいて算出される値を定義します。Vueのcomputedプロパティに非常に似ています。- 第一引数として
stateを受け取ることができます。 - アロー関数ではなく通常の関数として定義すれば、
this経由でそのストアのインスタンス(他のgetterを含む)にアクセスできます。
actions:- ユーザーの操作などに応じて
stateを更新するためのメソッドを定義します。Vueのmethodsに似ています。 action内ではthisを使ってstateや他のactionにアクセスできます。this.count++のように、直接stateを書き換えることができるのがPiniaのシンプルな点です。$patchメソッドを使うと、複数のstateを一度に効率的に更新できます。
- ユーザーの操作などに応じて
最後に、export const useCounterStore = ... のように、defineStoreが返す関数をエクスポートします。このuseCounterStore関数が、コンポーネントからストアを利用するための鍵となります。
3-3. コンポーネントでのStoreの利用
ストアを定義したら、次はVueコンポーネントでそれを使ってみましょう。src/App.vue(または任意のコンポーネント)を編集します。
“`vue
Piniaカウンター
現在のカウント: {{ counterStore.count }}
ユーザー名: {{ counterStore.userName }}
ダブルカウント: {{ counterStore.doubleCount }}
ダブルカウント+1: {{ counterStore.doubleCountPlusOne }}
{{ counterStore.displayMessage }}
“`
使い方は非常に直感的です。
1. import { useCounterStore } from '@/stores/counter' で、先ほど定義したストア関数をインポートします。
2. const counterStore = useCounterStore() で、ストアのインスタンスを生成します。この関数は、初回呼び出し時にストアを作成し、2回目以降は既存のインスタンスを返します。これにより、どのコンポーネントから呼び出しても同じシングルトンインスタンスにアクセスできます。
3. あとは counterStore.count のように、プロパティとしてstateやgetterにアクセスし、counterStore.increment() のように、メソッドとしてactionを呼び出すだけです。
分割代入とリアクティビティの問題 (storeToRefs)
開発を進めていると、テンプレートを簡潔にするために、ストアのプロパティを分割代入したくなることがあります。
typescript
// この書き方は問題があります!
const { count, doubleCount } = useCounterStore()
しかし、この方法には大きな落とし穴があります。このように直接分割代入すると、countやdoubleCountはリアクティビティを失ってしまいます。つまり、ストアのcountが更新されても、コンポーネントの表示は更新されません。これは、分割代入によって単なる値(number型など)がコピーされるだけで、Vueが変更を検知するためのリアクティブな接続(refやreactiveのラッパー)が失われるためです。
この問題を解決するために、PiniaはstoreToRefsという便利なユーティリティを提供しています。
“`vue
Piniaカウンター (storeToRefs使用)
現在のカウント: {{ count }}
ユーザー名: {{ userName }}
ダブルカウント: {{ doubleCount }}
“`
storeToRefsは、ストアインスタンスを受け取り、その中のstateとgettersの各プロパティをリアクティブなrefに変換したオブジェクトを返します。これにより、分割代入してもリアクティビティが維持され、期待通りに動作します。
一方で、actionsは単なるメソッドなので、リアクティビティは関係ありません。そのため、actionsはstoreToRefsを使わずに直接分割代入して問題ありません。
storeToRefsは、テンプレートの可読性を高めるために非常に有効なテクニックなので、ぜひ覚えておきましょう。
3-4. Setup Store: もう一つの書き方
これまで見てきたのは「Options Store」と呼ばれる形式でした。Piniaにはもう一つ、「Setup Store」という書き方があります。これはVueのComposition APIのsetup関数と非常によく似た構文でストアを定義する方法です。
src/stores/counterSetup.ts という新しいファイルを作成して、同じカウンター機能をSetup Storeで書いてみましょう。
“`typescript
// src/stores/counterSetup.ts
import { ref, computed } from ‘vue’
import { defineStore } from ‘pinia’
// 第二引数がオプションオブジェクトからセットアップ関数に変わる
export const useCounterSetupStore = defineStore(‘counterSetup’, () => {
// state -> ref()
const count = ref(0)
const userName = ref(‘Jane Doe’)
// getters -> computed()
const doubleCount = computed(() => count.value * 2)
const displayMessage = computed(() => {
return 現在のカウントは ${count.value}、ユーザー名は ${userName.value} です。
})
// actions -> function()
function increment() {
count.value++
}
function add(amount: number) {
count.value += amount
}
// ref, computed, function を return する必要がある
return { count, userName, doubleCount, displayMessage, increment, add }
})
“`
Options Storeとの違いは以下の通りです。
stateはref()で定義します。gettersはcomputed()で定義します。actionsはfunctionで定義します。- 最後に、コンポーネントや他のストアからアクセス可能にしたい
state,getters,actionsをオブジェクトとしてreturnします。
Options Store vs Setup Store: どちらを選ぶ?
どちらの形式を使っても機能的には同じです。選択は個人の好みやチームの規約によります。
- Options Store:
- 長所:
state,getters,actionsが明確に分離されており、構造がわかりやすい。Vue 2のOptions APIに慣れている人には馴染みやすい。thisを使うのが直感的。 - 短所:
thisの型推論が複雑な場合に問題が起きることが稀にある。
- 長所:
- Setup Store:
- 長所: Composition APIの知識をそのまま活かせる。
ref,computedを使うため、リアクティビティの仕組みがより明確。複雑なロジック(watchの使用など)をストア内に記述しやすい。 - 短所: 全てが同じスコープに存在するため、小規模なストアでは冗長に感じることがある。
- 長所: Composition APIの知識をそのまま活かせる。
初心者の方には、まず構造が明確なOptions Storeから始めることをお勧めします。 Composition APIに慣れてきたら、Setup Storeの柔軟性も試してみると良いでしょう。
4. Piniaの応用的な使い方をマスターする
基本をマスターしたら、次はより実践的なシナリオでPiniaを活用する方法を学びましょう。非同期処理、ストア間の連携、状態の購読など、Piniaが提供する強力な機能を使いこなせば、開発の幅が大きく広がります。
4-1. Actionsの詳細
非同期処理 (async/await)
現代のWebアプリケーションでは、APIサーバーからデータを取得する非同期処理が不可欠です。Piniaのactionsは、async/awaitをネイティブでサポートしているため、非同期処理を簡単に記述できます。
ユーザー情報をAPIから取得してストアに保存する例を見てみましょう。src/stores/user.tsを作成します。
“`typescript
// src/stores/user.ts
import { defineStore } from ‘pinia’
// ダミーのAPI関数
const fetchUserInfoAPI = (userId: string): Promise<{ name: string; email: string }> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ name: User ${userId}, email: user${userId}@example.com })
}, 1000) // 1秒の遅延をシミュレート
})
}
export const useUserStore = defineStore(‘user’, {
state: () => ({
user: null as { name: string; email: string } | null,
isLoading: false,
error: null as string | null,
}),
actions: {
async fetchUser(userId: string) {
this.isLoading = true
this.error = null
try {
// APIを呼び出し、結果を待つ
const userInfo = await fetchUserInfoAPI(userId)
// 成功したらstateを更新
this.user = userInfo
} catch (e) {
// エラーが発生したらエラー情報をstateに保存
this.error = ‘ユーザー情報の取得に失敗しました。’
} finally {
// 成功・失敗にかかわらず、ローディング状態を解除
this.isLoading = false
}
},
},
})
“`
このfetchUserアクションは以下のように動作します。
- アクションが呼び出されると、まず
isLoadingをtrueにして、UIにローディング中であることを示せるようにします。 try...catchブロックでAPI呼び出しを囲みます。awaitを使ってAPIからのレスポンスを待ちます。- API通信が成功すれば、取得したユーザー情報で
userstateを更新します。 - 通信が失敗すれば、
catchブロックでエラーメッセージをerrorstateに保存します。 finallyブロックは、成功・失敗にかかわらず実行され、isLoadingをfalseに戻します。
このように、非同期処理の各段階(開始、成功、失敗)に対応するstateを用意することで、コンポーネント側はisLoadingやerrorの状態を監視するだけで、リッチなUIを表現できます。
他のStoreのActionを呼び出す
アプリケーションが複雑になると、あるストアのアクションから別のストアのアクションを呼び出したくなることがあります。例えば、「商品をカートに追加(CartStore)したら、通知メッセージを表示(NotificationStore)する」といったケースです。
Piniaでは、アクション内で他のストアのuse関数を呼び出すことで、簡単にストア間の連携が実現できます。
“`typescript
// src/stores/cart.ts
import { defineStore } from ‘pinia’
import { useNotificationStore } from ‘./notification’ // 通知ストアをインポート
export const useCartStore = defineStore(‘cart’, {
state: () => ({
items: [] as string[],
}),
actions: {
addItem(itemName: string) {
this.items.push(itemName)
// 通知ストアのインスタンスを取得
const notificationStore = useNotificationStore()
// 通知ストアのアクションを呼び出す
notificationStore.showNotification(`「${itemName}」がカートに追加されました。`)
},
},
})
// src/stores/notification.ts
import { defineStore } from ‘pinia’
export const useNotificationStore = defineStore(‘notification’, {
state: () => ({
message: ”,
isVisible: false,
}),
actions: {
showNotification(msg: string) {
this.message = msg
this.isVisible = true
setTimeout(() => {
this.isVisible = false
}, 3000) // 3秒後に非表示
},
},
})
“`
cartStoreのaddItemアクション内でuseNotificationStore()を呼び出し、そのshowNotificationアクションを実行しています。これにより、ストアの責務を明確に分離したまま、協調して動作させることができます。
4-2. Gettersの詳細
引数を取るGetter
通常のgetterは引数を取れませんが、getterが関数を返すように実装することで、引数を受け取るgetterを擬似的に実現できます。これは、特定のIDを持つアイテムをリストから検索するような場合に非常に便利です。
“`typescript
// src/stores/todo.ts
import { defineStore } from ‘pinia’
interface Todo {
id: number
text: string
isFinished: boolean
}
export const useTodoStore = defineStore(‘todo’, {
state: () => ({
todos: [
{ id: 1, text: ‘Piniaを学ぶ’, isFinished: true },
{ id: 2, text: ‘Vue Routerを学ぶ’, isFinished: false },
{ id: 3, text: ‘Viteを学ぶ’, isFinished: false },
] as Todo[],
}),
getters: {
// 引数を取るgetter
getTodoById: (state) => {
// IDを引数に取る関数を返す
return (todoId: number) => state.todos.find((todo) => todo.id === todoId)
},
},
})
“`
コンポーネントでの使い方は以下のようになります。
“`vue
ID 2のTODO: {{ todoItem.text }}
``getTodoByIdは関数なので、()を付けて引数を渡して呼び出します。ただし、この方法で返される値は**キャッシュされない**ことに注意してください。呼び出されるたびにfindメソッドが実行されます。パフォーマンスが重要な場合は、コンポーネント側でcomputed`を使うなどの工夫が必要です。
4-3. Storeの購読 ($subscribe)
$subscribeメソッドを使うと、ストアのstateの変更を監視し、変更が発生するたびに特定の処理を実行できます。これは、watchのようにストアの変更をリアクティブに追跡する強力な機能です。
代表的なユースケースは、ストアの状態をブラウザのlocalStorageに保存し、永続化することです。
“`typescript
// どこかのコンポーネントの setup や main.ts などで実行
import { useCartStore } from ‘@/stores/cart’
const cartStore = useCartStore()
// cartStoreのstateが変更されるたびにコールバックが実行される
cartStore.$subscribe((mutation, state) => {
// mutation には変更に関する詳細情報が含まれる
// – type: ‘direct’ (直接代入), ‘patch object’, ‘patch function’
// – storeId: 変更があったストアのID
// – payload: $patchに渡されたオブジェクト(patchの場合)
// state には変更後のストアのstateオブジェクトが丸ごと入っている
console.log(‘Cart store changed:’, mutation)
// stateをlocalStorageに保存する
localStorage.setItem(‘cartState’, JSON.stringify(state))
})
“`
このコードをアプリケーションの初期化時(例: App.vueのonMountedフック)に一度だけ実行しておけば、cartStoreの状態が変更されるたびに、自動的にlocalStorageが更新されます。ページをリロードしてもカートの中身が復元されるようにするには、ストアの初期stateをlocalStorageから読み込む処理を追加します。
“`typescript
// src/stores/cart.ts
import { defineStore } from ‘pinia’
// localStorageから初期状態を読み込む
const initialState = JSON.parse(localStorage.getItem(‘cartState’) || ‘{“items”:[]}’)
export const useCartStore = defineStore(‘cart’, {
state: () => initialState,
// … actions …
})
“`
4-4. Actionの購読 ($onAction)
$subscribeがstateの変更を監視するのに対し、$onActionはactionの呼び出しを監視します。これにより、アクションの実行前、実行後、またはエラー発生時にフックをかけることができます。汎用的なロギングやエラーハンドリングに役立ちます。
“`typescript
import { useUserStore } from ‘@/stores/user’
const userStore = useUserStore()
const unsubscribe = userStore.$onAction(
({
name, // actionの名前
store, // ストアのインスタンス
args, // actionに渡された引数の配列
after, // actionが成功した後に実行されるフックを登録する関数
onError, // actionが失敗した後に実行されるフックを登録する関数
}) => {
// アクションが呼び出される直前に実行される
console.log(Action "${name}" が引数 ${args.join(', ')} で開始されました。)
// アクション成功後のフック
after((result) => {
console.log(`Action "${name}" が完了しました。結果:`, result)
})
// アクション失敗時のフック
onError((error) => {
console.error(`Action "${name}" でエラーが発生しました:`, error)
// ここで共通のエラートーストを表示するなどの処理が可能
})
}
)
// 購読を停止したい場合は、返された関数を実行する
// unsubscribe()
“`
この機能を使えば、例えば全てのアクションの実行時間を計測したり、特定のアクションでエラーが発生した場合にエラー報告サービスに情報を送信したりといった、横断的な関心事をエレガントに実装できます。
4-5. プラグインシステム
$subscribeや$onActionをさらに一般化し、再利用可能な形でPiniaの機能を拡張したい場合は、プラグインを作成します。プラグインは、ストアが作成されるときに一度だけ実行される関数です。
ここでは、すべてのストアの状態をlocalStorageに自動的に保存・復元する簡単なプラグインを作成してみましょう。
src/plugins/piniaLocalStorage.ts を作成します。
“`typescript
// src/plugins/piniaLocalStorage.ts
import { PiniaPluginContext } from ‘pinia’
export function piniaLocalStoragePlugin({ store }: PiniaPluginContext) {
// ストアの初期化時にlocalStorageからデータを復元
const storedState = localStorage.getItem(store.$id)
if (storedState) {
store.$patch(JSON.parse(storedState))
}
// ストアのstateが変更されたらlocalStorageに保存
store.$subscribe((_mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state))
})
}
“`
このプラグインは、PiniaPluginContextを引数に取る関数です。コンテキストにはstoreインスタンスが含まれており、これを使ってストアに新しいプロパティを追加したり、$subscribeを登録したりできます。
作成したプラグインを main.ts で登録します。
“`typescript
// src/main.ts
import { createApp } from ‘vue’
import { createPinia } from ‘pinia’
import { piniaLocalStoragePlugin } from ‘./plugins/piniaLocalStorage’ // プラグインをインポート
import App from ‘./App.vue’
const pinia = createPinia()
pinia.use(piniaLocalStoragePlugin) // プラグインを登録
const app = createApp(App)
app.use(pinia)
app.mount(‘#app’)
“`
これで、定義した全てのストア(counter, userなど)が、自動的にlocalStorageとの間で状態を同期するようになります。個別のストアで同じロジックを繰り返し書く必要がなくなり、コードが非常にクリーンになります。
5. 実践的なサンプル:TODOリストアプリケーションの作成
これまでに学んだ知識を総動員して、Piniaを使った実践的なTODOリストアプリケーションを作成してみましょう。このサンプルを通じて、複数のstate, getters, actionsが連携して一つの機能を完成させる流れを体験します。
要件:
* タスクの追加ができる
* タスクの削除ができる
* タスクの完了/未完了状態を切り替えられる
* 表示するタスクを「すべて」「完了済み」「未完了」でフィルタリングできる
Storeの設計 (src/stores/todoList.ts)
まず、TODOリストのロジック全体を管理するストアを設計します。
“`typescript
// src/stores/todoList.ts
import { defineStore } from ‘pinia’
import { ref, computed } from ‘vue’
// Todoアイテムの型定義
export interface Todo {
id: number
text: string
isFinished: boolean
}
// フィルタの状態を表す型
export type Filter = ‘all’ | ‘finished’ | ‘unfinished’
// Setup Store形式で定義してみましょう
export const useTodoListStore = defineStore(‘todoList’, () => {
// — state —
const todos = ref
const nextId = ref(0)
const filter = ref
// — getters —
const finishedTodos = computed(() => todos.value.filter((todo) => todo.isFinished))
const unfinishedTodos = computed(() => todos.value.filter((todo) => !todo.isFinished))
const filteredTodos = computed(() => {
switch (filter.value) {
case ‘finished’:
return finishedTodos.value
case ‘unfinished’:
return unfinishedTodos.value
default:
return todos.value
}
})
// — actions —
function addTodo(text: string) {
if (!text.trim()) return
todos.value.push({
id: nextId.value++,
text: text,
isFinished: false,
})
}
function removeTodo(id: number) {
todos.value = todos.value.filter((todo) => todo.id !== id)
}
function toggleTodoStatus(id: number) {
const todo = todos.value.find((todo) => todo.id === id)
if (todo) {
todo.isFinished = !todo.isFinished
}
}
function setFilter(newFilter: Filter) {
filter.value = newFilter
}
return {
todos,
filter,
filteredTodos,
addTodo,
removeTodo,
toggleTodoStatus,
setFilter,
}
})
“`
このストアはSetup Store形式で記述しました。refでリアクティブなstateを、computedで派生的なgettersを、functionでactionsを定義しています。filteredTodos getterが、現在のfilter stateに応じて表示すべきTODOリストを動的に計算している点がポイントです。
コンポーネントの実装
次に、このストアを利用するUIコンポーネントを作成します。App.vueを以下のように編集します。
“`vue
Pinia TODO List
-
{{ todo.text }}
タスクはありません。
“`
このコンポーネントでは、ストアから取得したstate, getters, actionsをテンプレート内でフル活用しています。
- タスク追加: 入力されたテキスト (
newTodoText) をaddTodoアクションに渡しています。 - タスク表示:
v-forでfilteredTodosゲッターの結果をループ表示しています。これにより、フィルタが変更されると表示内容が自動的に更新されます。 - 状態変更: タスクのテキストをクリックすると
toggleTodoStatusが、削除ボタンをクリックするとremoveTodoが、それぞれ対応するIDを引数にして呼び出されます。 - フィルタリング: 各フィルタボタンがクリックされると
setFilterアクションが呼び出され、ストアのfilterstateが更新されます。filterstateが変わるとfilteredTodosゲッターが再計算され、UIに即座に反映されます。
このように、コンポーネントは「ユーザーの入力をストアのアクションに伝える」ことと「ストアの状態をUIに表示する」という2つの役割に専念しています。実際のロジック(タスクの追加方法、フィルタリングの計算方法など)はすべてストアにカプセル化されているため、コンポーネントは非常に薄く、見通しが良くなっています。これこそが、状態管理ライブラリを使う最大のメリットです。
6. まとめ:Piniaと共に歩むVue開発の未来
この記事では、Vueの新しい公式状態管理ライブラリであるPiniaについて、その基本から応用、そして実践的なサンプルまでを徹底的に解説してきました。
- Piniaはシンプル、型安全、拡張性を兼ね備えたモダンなライブラリであること。
Mutationsを廃止し、State,Getters,Actionsだけで直感的に状態を管理できること。defineStoreでストアを定義し、コンポーネント内でuseStore()フックを使って簡単に利用できること。- リアクティビティを維持して分割代入するための
storeToRefsの重要性。 - 非同期処理、ストア間連携、購読(
$subscribe)、プラグインといった強力な応用機能。
Piniaは、Vuexが長年培ってきた状態管理のベストプラクティスを継承しつつ、Composition API時代の開発スタイルに合わせて再設計された、まさに「Vueの新しい常識」です。その学習コストの低さとTypeScriptとの親和性の高さは、小規模な個人プロジェクトから大規模なエンタープライズアプリケーションまで、あらゆる規模のVue開発において強力な味方となるでしょう。
状態管理は、かつては初心者にとって高いハードルの一つでした。しかし、Piniaの登場により、そのハードルは劇的に下がりました。この記事を通じて、あなたがPiniaへの第一歩を踏み出し、そのシンプルさと強力さを実感できたなら幸いです。
ぜひ、あなたの次のVueプロジェクトでPiniaを試してみてください。そして、よりクリーンで、よりメンテナンスしやすく、より楽しいVue開発の世界を体験してください。Piniaと共に、あなたの開発ライフがより豊かなものになることを願っています。
さらに学ぶために:
* Pinia公式ドキュメント: 全てのAPIと概念について最も正確で詳細な情報が記載されています。困ったときには必ず参照しましょう。