Piniaとは?Vue.jsで学ぶ状態管理の基本と導入方法
Vue.jsアプリケーションを開発する上で、コンポーネント間でデータを共有したり、アプリケーション全体で状態を一元管理したりする必要が出てくる場面は多々あります。小規模なアプリケーションであれば、Propsによるデータの受け渡しやイベントの発行で事足りるかもしれません。しかし、アプリケーションが複雑になり、コンポーネントツリーが深くなるにつれて、データの受け渡しは煩雑になり、「Propsバケツリレー」や「イベントの連鎖」といった問題が発生しやすくなります。
このような問題を解決するために登場するのが「状態管理ライブラリ」です。Vue.jsの世界では、長らくVuexが標準的な状態管理ライブラリとして利用されてきました。そして、Vue 3の登場と共に、Vuexの後継として開発され、よりシンプルで直感的になったのが「Pinia」です。
本記事では、Piniaとは何か、なぜ状態管理が必要なのかといった基本的な部分から始まり、Piniaの導入方法、ストアの作成と利用、さらには実践的な使い方までを、詳細なコード例と共に解説します。Vue.jsでアプリケーション開発を行うすべての方にとって、Piniaを使った効率的で保守しやすい状態管理を学ぶための決定版となることを目指します。
1. はじめに:状態管理の必要性とPiniaの登場
1.1 Vue.jsアプリケーションにおける状態管理の重要性
Vue.jsはコンポーネントベースのUIフレームワークです。アプリケーションは様々なコンポーネントの組み合わせによって構成されます。各コンポーネントは自身のデータ(ローカルステート)を持っています。
しかし、複数のコンポーネントで共有したいデータや、アプリケーション全体で一貫して管理したいデータ(例えば、認証状態、ユーザー情報、テーマ設定、APIから取得したデータなど)が出てきた場合、どのように管理すれば良いでしょうか?
- Propsによる受け渡し: 親コンポーネントから子コンポーネントへデータを渡す標準的な方法です。しかし、孫、曽孫とコンポーネントツリーが深くなるにつれて、データを必要としない中間コンポーネントにもPropsを渡さなければならなくなる「Propsバケツリレー」が発生し、コードの見通しが悪化し、保守が困難になります。
- イベントによる通知: 子コンポーネントから親コンポーネントへ変更を通知する標準的な方法です。しかし、遠い祖先コンポーネントの状態を変更したい場合など、イベントの発行とリスニングが複雑になりがちです。
- グローバル変数やシングルトン: JavaScriptの単純なオブジェクトをグローバルに置いて共有する方法も考えられますが、どのコンポーネントがいつそのデータを変更したのか追跡することが難しくなり、デバッグ性が著しく低下します。
これらの問題は、アプリケーションが大きくなるにつれて顕著になります。状態管理ライブラリは、これらの課題を解決し、アプリケーションの状態を予測可能な方法で管理するための仕組みを提供します。
1.2 状態管理ライブラリの歴史:VuexからPiniaへ
Vue.jsの公式状態管理ライブラリとして、長らくVuexが利用されてきました。Vuexは、状態(State)、ミューテーション(Mutations)、アクション(Actions)、ゲッター(Getters)、モジュール(Modules)といった概念を導入し、大規模アプリケーションでの状態管理に貢献しました。
しかし、Vuexにはいくつか改善の余地がある点も指摘されていました。特に、TypeScriptとの親和性の問題や、Mutationsの必要性(状態変更は必ずMutation経由で行うというルール)、そして設定の煩雑さなどが挙げられます。
Vue 3のComposition APIの登場は、状態管理のアプローチにも新たな可能性をもたらしました。Composition APIでリアクティブな状態を定義するのと同じように、アプリケーション全体で共有する状態を定義できれば、よりシンプルで柔軟な状態管理が実現できるのではないか?という考えが生まれました。
Piniaは、まさにこのような背景から生まれました。Vue 3のComposition APIの考え方を取り入れつつ、Vuexの優れた点を継承し、さらに改善を加えた新しい状態管理ライブラリです。Vue 3の公式状態管理ライブラリとなり、現在ではVue.jsプロジェクトにおける推奨の状態管理ソリューションとなっています。
1.3 Piniaとは何か?その特徴と利点
Piniaは、Vue.jsのための軽量かつシンプルな状態管理ライブラリです。Vue 3との連携を前提に設計されており、Composition APIと非常に高い親和性を持っています。
Piniaの主な特徴と利点は以下の通りです。
- シンプルさ: Vuexに比べてAPIが非常にシンプルです。特に、Mutationの概念がなくなり、状態変更はすべてActionで行います。
- 直感的なストア定義:
defineStore
という関数を使ってストアを定義します。Options APIライクな書き方と、Setup APIライクな書き方の両方が可能です。 - TypeScriptとの高い親和性: 最初からTypeScriptで書かれており、型推論が強力に働くため、TypeScriptプロジェクトでの開発体験が非常に優れています。状態、ゲッター、アクションの型付けが容易です。
- 軽量: Vuexに比べてバンドルサイズが小さいです。
- モジュール化: ストアは最初からモジュール化されています。名前空間の衝突を心配することなく、複数のストアを定義してアプリケーションを分割できます。
- Vue Devtoolsのサポート: Vue DevtoolsのPiniaプラグインを使うことで、ストアの状態の確認、状態変更のタイムトラベルデバッグ、パフォーマンス監視などが可能です。
- Vue 3 Composition APIとの統合: Setup APIスタイルのストア定義は、Composition APIでカスタムフックを作るような感覚で記述できます。
storeToRefs
のようなヘルパー関数も提供されています。 - Vue 2対応: Pinia v1系はVue 2にも対応しています(ただし、Vue 3との組み合わせが最も推奨されます)。Vue 3移行を検討しているプロジェクトでも段階的に導入しやすいかもしれません。
これらの特徴により、PiniaはVue.jsプロジェクトにおける状態管理をよりシンプルに、より効率的に、そしてより堅牢なものにしてくれます。
2. 状態管理の基本
Piniaの具体的な使い方に入る前に、改めて状態管理の基本的な概念について理解を深めましょう。
2.1 状態(State)とは何か
Webアプリケーションにおける「状態」とは、アプリケーションの動作や見た目を決定するデータの集合体です。例えば、ユーザーがログインしているかどうか、ショッピングカートに入っている商品のリスト、現在表示しているページのID、UI要素の表示/非表示などが状態にあたります。
これらの状態は、ユーザーの操作やサーバーからのレスポンスなどによって時間と共に変化します。状態の変化に応じて、UIが更新されたり、別の処理が実行されたりします。
2.2 単一真実の源(Single Source of Truth)の概念
状態管理ライブラリが目指す理想の一つに「単一真実の源(Single Source of Truth, SSOT)」があります。これは、アプリケーション全体で共有されるすべての状態を、一箇所で集中管理するという考え方です。
状態が様々なコンポーネメントに分散していると、同じデータなのに複数のコピーが存在したり、どのデータが最新で正しいのか分からなくなったりします。SSOTの原則に基づき状態を集中管理することで、データの矛盾を防ぎ、アプリケーションの状態を常に信頼できるものにすることができます。
Piniaでは、「ストア(Store)」がこの単一真実の源となります。
2.3 コンポーネント間のデータの受け渡し(Props vs State Management)
先述の通り、Vue.jsでは親から子へのデータ渡しにProps、子から親への通知にイベントが標準的な方法です。これは、コンポーネント間の親子関係が明確な場合に有効です。
しかし、親子の関係がないコンポーネント間でのデータ共有や、コンポーネントツリーの遠く離れたコンポーネント間でのデータ共有は、Propsやイベントだけでは難しくなります。
状態管理ライブラリを使うと、コンポーネントツリーのどこに位置するコンポーネントであっても、ストアを経由して共有状態にアクセスしたり、変更したりできるようになります。これにより、Propsバケツリレーや複雑なイベントの連鎖を回避できます。
2.4 状態管理ライブラリを使わない場合の課題の再確認
状態管理ライブラリを使わずに、カスタムのJavaScriptオブジェクトやProvide/Injectのような機能だけで複雑な状態管理を行おうとすると、以下のような課題に直面しやすいです。
- 追跡困難な状態変更: どのコンポーネントがいつ、どのような理由で状態を変更したのかを追跡するのが難しい。デバッグが困難になる。
- 状態の同期問題: 同じ状態の複数のコピーが存在する場合、それらを常に同期させ続けるのが難しい。
- コードの見通しの悪化: 状態に関連するロジックがコンポーネントに分散し、全体像を把握しにくくなる。
- テストの難しさ: 状態とコンポーネントの依存関係が複雑になり、単体テストが書きにくくなる。
Piniaのような状態管理ライブラリを導入することで、これらの課題を解決し、アプリケーションをスケーラブルで保守しやすいものにすることができます。
3. Piniaの核心概念
Piniaは、Vuexの概念をよりシンプルに再構築しています。Piniaの主な構成要素は以下の3つです。
- State (状態): アプリケーションの核となるデータ。リアクティブなオブジェクトとして保持されます。
- Getters (ゲッター): Stateから派生した値(算出プロパティ)。Stateを加工したりフィルタリングしたりした結果を返します。Stateが変更されると自動的に再計算されます。
- Actions (アクション): Stateを変更するためのロジックを含むメソッド。API呼び出しのような非同期処理もここで行います。MutationはPiniaには存在せず、状態変更はActions内で直接、またはGettersを経由せずに行います。
VuexにあったMutationがPiniaでは廃止されたことが大きな違いです。Vuexでは、状態変更は必ずMutationを介して行う必要がありましたが、PiniaではAction内で直接状態を更新できます。これにより、概念が一つ減り、よりシンプルになりました。状態変更の追跡は、Vue Devtoolsを使えばActionの実行履歴として確認できます。
これらのState, Getters, Actionsをひとまとめにしたものが「ストア(Store)」です。アプリケーションは一つまたは複数のストアを持ち、それぞれのストアが特定のドメイン(例: ユーザー認証、商品リスト、設定など)の状態を管理します。
4. Piniaの導入方法
Vue.jsプロジェクトにPiniaを導入するのは非常に簡単です。既存のプロジェクトにも新規プロジェクトにも容易に組み込めます。
4.1 プロジェクトへのインストール
まず、npm、yarn、またはpnpmを使ってプロジェクトにPiniaをインストールします。
“`bash
npmの場合
npm install pinia
yarnの場合
yarn add pinia
pnpmの場合
pnpm add pinia
“`
4.2 プラグインとしての登録(main.js/ts)
Piniaをインストールしたら、Vueアプリケーションで使えるようにプラグインとして登録します。通常はアプリケーションのエントリーファイル(main.js
やmain.ts
)で行います。
“`javascript
// src/main.js (JavaScriptの場合)
import { createApp } from ‘vue’
import { createPinia } from ‘pinia’
import App from ‘./App.vue’
// Piniaインスタンスを作成
const pinia = createPinia()
const app = createApp(App)
// VueアプリにPiniaを登録
app.use(pinia)
app.mount(‘#app’)
“`
“`typescript
// src/main.ts (TypeScriptの場合)
import { createApp } from ‘vue’
import { createPinia } from ‘pinia’
import App from ‘./App.vue’
// Piniaインスタンスを作成
const pinia = createPinia()
const app = createApp(App)
// VueアプリにPiniaを登録
app.use(pinia)
app.mount(‘#app’)
“`
これで、アプリケーション全体でPiniaのストアを利用できるようになります。
4.3 最初のストアの作成(defineStore)
Piniaのストアは、defineStore
関数を使って定義します。この関数は、ストアのIDとオプションオブジェクト(またはセットアップ関数)を受け取ります。
ストアIDは、アプリケーション全体で一意である必要があります。通常は文字列で指定し、開発ツールでの識別に使われます。
defineStore
の第2引数には、以下の2つのスタイルでストアの定義を記述できます。
- Options APIスタイル:
state
,getters
,actions
というプロパティを持つオブジェクトを渡す方法。VuexのOptions APIに似ています。 - Setup APIスタイル: セットアップ関数を渡す方法。Composition APIのように、
ref
,computed
, 関数を使って状態、ゲッター、アクションを定義します。
まずはOptions APIスタイルで簡単なカウンターストアを作成してみましょう。
src/stores/counter.js
(または src/stores/counter.ts
) のようなファイルを作成し、以下のコードを記述します。
“`javascript
// src/stores/counter.js (JavaScript Options API Style)
import { defineStore } from ‘pinia’
export const useCounterStore = defineStore(‘counter’, {
// State: アプリケーションの状態を定義する関数
state: () => ({
count: 0,
}),
// Getters: Stateから派生した値(算出プロパティ)
getters: {
doubleCount: (state) => state.count * 2,
// 他のゲッターにもアクセス可能
// doubleCountPlusOne: (state) => state.doubleCount + 1 // これはエラー、thisを使う必要がある
doubleCountPlusOne(): number {
return this.doubleCount + 1; // thisを使う
}
},
// Actions: Stateを変更するためのメソッド
actions: {
increment() {
this.count++
},
decrement() {
this.count–
},
incrementBy(amount) {
this.count += amount
},
async incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000))
this.count++
}
},
})
“`
“`typescript
// src/stores/counter.ts (TypeScript Options API Style)
import { defineStore } from ‘pinia’
interface CounterState {
count: number
}
export const useCounterStore = defineStore(‘counter’, {
// State: 型をジェネリクスで指定することも可能
state: (): CounterState => ({
count: 0,
}),
// Getters: 型推論が効く。引数にstateを受け取るか、thisを使う
getters: {
doubleCount: (state: CounterState): number => state.count * 2,
doubleCountPlusOne(): number {
return this.doubleCount + 1; // thisを使うことで他のゲッターにアクセスできる
},
},
// Actions: 非同期処理も可能。thisでStateや他のActionにアクセスできる
actions: {
increment() {
this.count++; // Stateを直接変更
},
decrement() {
this.count–;
},
incrementBy(amount: number) {
this.count += amount;
},
async incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000));
this.count++;
}
},
})
“`
defineStore
の戻り値は関数です。この関数(ここでは useCounterStore
)をコンポーネント内で呼び出すことで、ストアのインスタンスを取得し、State, Getters, Actionsにアクセスできるようになります。関数名には慣習として use
プレフィックスを付けます。
4.4 コンポーネントでのストアの使用(useStore)
作成したストアは、Vueコンポーネントの<script setup>
やsetup()
関数、またはOptions APIのメソッドなどで使用できます。ストアを使用するには、先ほど定義した関数(例: useCounterStore
)を呼び出します。
“`vue
Counter
Count: {{ count }}
Double Count: {{ doubleCount }}
Double Count Plus One: {{ doubleCountPlusOne }}
“`
重要な注意点:
ストアから取得したStateやGetterのプロパティ(例: counterStore.count
)は、そのまま分割代入(const { count } = counterStore
)するとリアクティブ性が失われます。これは、PiniaストアインスタンスがProxyオブジェクトであり、そのプロパティへの直接アクセスはリアクティブですが、分割代入によって元のProxyオブジェクトから切り離されたプリミティブ値などが取得されるためです。
StateやGetterをリアクティブな参照としてコンポーネントで使用したい場合は、Piniaが提供するstoreToRefs
ヘルパー関数を使用します。storeToRefs
は、ストアのStateとGetterのプロパティを、リアクティブなref
の集合に変換して返します。これにより、分割代入してもリアクティブ性が維持されます。
一方、Actionは単なる関数なので、分割代入しても問題ありません。
“`vue
Counter
Count: {{ count }}
Double Count: {{ doubleCount }}
Double Count Plus One: {{ doubleCountPlusOne }}
“`
こちらの方がよりすっきりして、Setup APIのスタイルに馴染みます。
これで、Piniaのストアを定義し、コンポーネントで使用する基本的な流れを理解しました。
5. ストアの定義と使用の詳細
Piniaのストア定義スタイルであるOptions APIとSetup APIについて、さらに詳しく見ていきましょう。
5.1 Options APIスタイルの詳細
Options APIスタイルは、VuexやVueコンポーネントのOptions APIに慣れている開発者にとって馴染みやすい形式です。state
, getters
, actions
という明確に区切られたプロパティの中に、それぞれのロジックを記述します。
“`javascript
// Options API Style
export const useSomeStore = defineStore(‘someStoreId’, {
state: () => ({
// リアクティブな状態データ
property1: ‘initial value’,
property2: 0,
}),
getters: {
// Stateから派生した値
derivedValue(state) {
return state.property2 * 10;
},
// 他のゲッターやStateにアクセスするには this を使う
anotherDerivedValue(): string {
return this.property1.toUpperCase();
}
},
actions: {
// 状態を変更するメソッド
changeProperty1(newValue) {
this.property1 = newValue;
},
incrementProperty2(amount) {
this.property2 += amount;
},
// 非同期アクション
async fetchData(id) {
// 例: API呼び出し
const response = await fetch(/api/data/${id}
);
const data = await response.json();
this.property1 = data.name; // Stateを更新
}
},
})
“`
state
:- 状態を定義する関数です。この関数はオブジェクトを返します。
- 関数である理由は、ストアの複数のインスタンスを作成できるようにするためです。関数が返す新しいオブジェクトが、それぞれのストアインスタンスの状態として使われます。
- 返されるオブジェクトのプロパティは、自動的にリアクティブになります(Vue 3の
reactive
と同じ仕組み)。
getters
:- Stateから派生した値(算出プロパティ)を定義するオブジェクトです。
- 各ゲッターは関数として定義します。最初の引数として
state
オブジェクトを受け取ることもできます。 - 他のゲッターやStateにアクセスしたい場合は、
this
を使用します。this
はストアインスタンスを指します。TypeScriptの場合、this
を使うゲッターには戻り値の型アノテーションが必要です(doubleCountPlusOne(): number { ... }
のように)。 - ゲッターは算出プロパティなので、依存するStateや他のゲッターが変更されない限り、結果はキャッシュされます。
- ゲッターは状態を取得するためのものであり、状態を変更するべきではありません。
actions
:- 状態を変更するためのメソッドを定義するオブジェクトです。
- 各アクションは関数として定義します。
this
を使うことで、State, Getters, 他のActionsにアクセスできます。 - アクション内でStateを直接変更できます (
this.count++
のように)。VuexのようなMutationを挟む必要はありません。 - 非同期処理(API呼び出しなど)はアクション内で行うのが一般的です。
async
/await
構文をそのまま使えます。 - 他のアクションを呼び出すことも可能です (
this.someOtherAction()
)。
Options APIスタイルは、Vuexからの移行や、Props, Events, Computed Propertyといった基本的なVueの概念に慣れている場合に理解しやすいでしょう。
5.2 Setup APIスタイルの詳細
Setup APIスタイルは、Vue 3のComposition APIのsetup()
関数に似ています。defineStore
の第2引数として関数を渡し、その関数内でref
, reactive
, computed
, 関数を使って状態、ゲッター、アクションを定義し、それらをオブジェクトとして返します。
“`javascript
// Setup API Style
import { defineStore } from ‘pinia’
import { ref, computed } from ‘vue’ // ref, computedはVueからインポート
export const useSomeStore = defineStore(‘someStoreId’, () => {
// State: ref または reactive で定義
const property1 = ref(‘initial value’) // プリミティブ型は ref
const property2 = ref(0)
const someObject = reactive({ nested: ‘value’ }) // オブジェクトや配列は reactive
// Getters: computed で定義
const derivedValue = computed(() => property2.value * 10) // .value が必要
const anotherDerivedValue = computed(() => property1.value.toUpperCase()) // .value が必要
// Actions: 通常の関数として定義
function changeProperty1(newValue) {
property1.value = newValue; // .value が必要
}
function incrementProperty2(amount) {
property2.value += amount; // .value が必要
}
// 非同期アクション
async function fetchData(id) {
// 例: API呼び出し
const response = await fetch(/api/data/${id}
);
const data = await response.json();
property1.value = data.name; // State (ref) を更新
}
// 他のアクションを呼び出す場合
function someActionCallingOther() {
// 他のアクションを定義した関数を直接呼び出す
incrementProperty2(10);
changeProperty1(‘updated’);
}
// 外に公開したい State, Getters, Actions をオブジェクトとして返す
return {
property1,
property2,
someObject,
derivedValue,
anotherDerivedValue,
changeProperty1,
incrementProperty2,
fetchData,
someActionCallingOther,
}
})
“`
“`typescript
// src/stores/someStore.ts (TypeScript Setup API Style)
import { defineStore } from ‘pinia’
import { ref, computed, reactive } from ‘vue’
export const useSomeStore = defineStore(‘someStoreId’, () => {
// State: 明示的な型付け
const property1 = ref
const property2 = ref
interface SomeObjectState {
nested: string;
}
const someObject: SomeObjectState = reactive({ nested: ‘value’ })
// Getters: computed + 明示的な戻り値型付け (型推論も強力に効く)
const derivedValue = computed
const anotherDerivedValue = computed
// Actions: 通常の関数 + 引数/戻り値型付け
function changeProperty1(newValue: string) {
property1.value = newValue
}
function incrementProperty2(amount: number) {
property2.value += amount
}
async function fetchData(id: number | string) {
const response = await fetch(/api/data/${id}
);
const data = await response.json(); // 仮定として data に name プロパティがある
property1.value = data.name as string; // 型アサーションまたは適切な型ガード
}
function someActionCallingOther() {
incrementProperty2(10);
changeProperty1(‘updated’);
}
// 外に公開したいものをオブジェクトとして返す
return {
// State は生の ref/reactive を返す (storeToRefs 不要)
property1,
property2,
someObject,
// Getters は computed の結果を返す (storeToRefs 不要)
derivedValue,
anotherDerivedValue,
// Actions は関数を返す
changeProperty1,
incrementProperty2,
fetchData,
someActionCallingOther,
}
})
“`
- State:
ref
またはreactive
を使って定義します。これらの関数はVueからインポートします。ref
で定義されたStateは、アクセスや変更の際に.value
が必要です。reactive
で定義されたオブジェクトの状態は、.value
なしでアクセスできます。
- Getters:
computed
を使って定義します。computed
もVueからインポートします。computed
関数はゲッター関数を受け取り、その中でState(ref
の場合は.value
が必要)を使って算出値を計算します。- Setup APIスタイルでは、
this
は使えません。他のゲッターにアクセスしたい場合は、対応するcomputed
変数を直接参照します。
- Actions:
- 通常のJavaScript関数として定義します。
- アクション内では、定義したState(
ref
の場合は.value
が必要)やゲッター(computed
変数。.value
が必要)を直接参照できます。 - 他のアクションを呼び出す場合も、定義した関数を直接呼び出します。
- 非同期処理もそのまま記述できます。
- 戻り値:
- セットアップ関数は、コンポーネントから利用可能にしたいState, Getters, Actionsをまとめたオブジェクトを返します。
- Setup APIスタイルで定義されたストアをコンポーネントで使用する場合、
storeToRefs
ヘルパー関数は通常不要です。State(ref
やreactive
)やGetters(computed
)は、返されたオブジェクトに含まれる時点で既にリアクティブな参照またはオブジェクトそのものであるため、分割代入してもリアクティブ性が維持されます。(ただし、返すオブジェクトのプロパティ名を変更したい場合など、storeToRefs
が便利なケースもあります。)
5.3 両スタイルの比較と使い分け
特徴 | Options APIスタイル | Setup APIスタイル |
---|---|---|
State定義 | state: () => ({...}) |
ref , reactive |
Getters定義 | getters: {...} (関数、this 使用可能) |
computed (.value でState参照) |
Actions定義 | actions: {...} (メソッド、this 使用可能) |
通常の関数 (.value でState/Getter参照) |
this の使用 |
State, Getters, Actions プロパティへのアクセスに利用 | 使用不可 |
リアクティブな取得 (コンポーネント) | storeToRefs が必要 |
通常不要 (返される値が既にリアクティブ) |
Vuexとの類似 | 高い | Composition APIとの類似性が高い |
TypeScript | this を使う場合に型付けが必要になることがある |
型推論が強力に働きやすいが、.value に注意 |
記述量 | やや定型的 | より自由、同じ処理でも複数の書き方があり得る |
柔軟性 | 定義されたプロパティの範囲内に限られる | より高い (Setup関数の自由度) |
どちらのスタイルを選ぶべきか?
- Options APIスタイル:
- VuexやVue Options APIに慣れている場合。
- ストアの構造が比較的シンプルで、State, Getters, Actionsの区分が明確な場合。
- チーム内でOptions APIスタイルに統一したい場合。
- Setup APIスタイル:
- Vue 3のComposition APIを積極的に利用している場合。
- ストア内のロジックが複雑で、より柔軟な記述が必要な場合(例: ウォッチャー、ライフサイクルのフックに類する処理など – Piniaストア自体にVueコンポーネントのようなライフサイクルはないが、プラグインなどで似たような処理を行う場合にSetupスタイルが役立つことがある)。
- TypeScriptの恩恵を最大限に受けたい場合(Setupスタイルの方が型推論が強力に働く傾向がある)。
- Vueコンポーネントの
<script setup>
と同様の記述スタイルで統一したい場合。
どちらのスタイルを使っても、Piniaの機能に大きな違いはありません。プロジェクトやチームの慣習に合わせて選択すれば良いでしょう。ただし、一つのプロジェクト内ではスタイルを統一することをお勧めします。Vueのドキュメントやコミュニティでは、Vue 3においてはComposition API(Setupスタイル)が推奨される傾向にあります。本記事でも以降、特に指定がない限りはSetup APIスタイルのストア定義を中心に解説します。
6. コンポーネントでのストアの使用 (詳細)
コンポーネントでPiniaストアを使う方法をさらに掘り下げて説明します。主にSetup API (<script setup>
) での使用を想定しますが、Options APIでの使い方にも少し触れます。
6.1 ストアインスタンスの取得 (useStore
)
コンポーネント内でストアにアクセスするには、ストア定義ファイルでエクスポートした関数(例: useCounterStore
)を呼び出します。
“`vue
“`
この counterStore
オブジェクトは、Piniaによって注入されたシングルトン(アプリケーション全体で一つのインスタンス)です。このオブジェクトを通じて、ストアのState, Getters, Actionsにアクセスします。
6.2 Stateへのアクセス
Options APIスタイルで定義されたストアの場合、Stateはストアインスタンスの.state
プロパティを通じてアクセスできます。
javascript
// Options APIスタイルのストアを想定
const counterStore = useCounterStore();
const count = counterStore.count; // OKだが、そのまま使うとリアクティブではない!
Setup APIスタイルで定義されたストアの場合、Setup関数が返すオブジェクトに含まれるStateプロパティに直接アクセスします。
javascript
// Setup APIスタイルのストアを想定
const counterStore = useCounterStore();
const count = counterStore.count; // これでリアクティブな ref にアクセスできる
リアクティブ性の維持 – storeToRefs
前述の通り、Options APIスタイルのストアからStateプロパティを分割代入などで取り出すと、リアクティブ性が失われます。Setup APIスタイルでも、ストア定義のSetup関数でStateをref
やreactive
として返し、それをコンポーネント側で受け取る分にはリアクティブですが、もし何らかの理由でストアインスタンスそのものを渡し、そこからプロパティを取り出すようなケースでは同様の問題が発生する可能性があります。
Piniaでは、StateやGetterをリアクティブな参照としてコンポーネントのテンプレートや算出プロパティ、ウォッチャーなどで使用したい場合に、storeToRefs
ヘルパー関数を使用します。
“`vue
Count: {{ count }}
“`
storeToRefs(storeInstance)
は、storeInstance.state
と storeInstance.getters
からすべてのプロパティを取り出し、それぞれのプロパティを新しいリアクティブなref
として持つオブジェクトを返します。このオブジェクトを分割代入することで、元のストアの状態と同期されたリアクティブな参照が得られます。
Setup APIスタイルのストアでStateやGetterを定義し、それをストア定義のSetup関数の戻り値として返す場合、通常 storeToRefs
は不要です。
javascript
// Setup APIスタイルのストア定義の例
export const useMyStore = defineStore('myStore', () => {
const myState = ref('initial');
const myGetter = computed(() => myState.value + ' computed');
// ... actions ...
return { myState, myGetter /* ...actions */ }; // ref/computedをそのまま返す
});
“`vue
{{ myState }}
{{ myGetter }}
``
このため、Composition API (
Double Count: {{ doubleCount }}
```
6.4 Actionsの呼び出し
Actionsは状態を変更するためのメソッドです。ストアインスタンスのプロパティとして関数として提供されます。
Actionsは関数なので、呼び出す際にリアクティブ性は関係ありません。ストアインスタンスから直接呼び出すか、分割代入で取り出した関数を呼び出します。
```vue
```
Actionsは引数を受け取ったり、Promiseを返したりすることも可能です。非同期Actionの場合、呼び出し元でawait
を使って完了を待つこともできます。
6.5 Options APIコンポーネントでのストアの使用
Vue 3でもOptions APIスタイルのコンポーネントを使用している場合、Piniaストアはthis
を通じてアクセスできます。ただし、StateやGetterをリアクティブに使うためには、Computedプロパティ内でストアのプロパティを参照するか、mapStores
, mapState
, mapGetters
, mapActions
のようなヘルパーが必要になります。
Vuex 4.x/Piniaの公式ドキュメントでは、Vuex 3.xまで存在したmapState
のようなヘルパーは推奨されていません。これは、Composition APIとの親和性や、型推論の問題があるためです。Piniaを使う場合は、可能な限りSetup API (<script setup>
) スタイルでコンポーネントを記述し、前述のstoreToRefs
を使った方法を採用するのが良いでしょう。
もしOptions APIコンポーネントでPiniaストアを使いたい場合は、Computedプロパティでストアの状態を参照するのが一般的な方法です。
```vue
Count: {{ count }}
Double Count: {{ doubleCount }}
```
この方法では、Computedプロパティの中で毎回useCounterStore()
を呼び出す必要がありますが、Piniaは内部でストアインスタンスをキャッシュしているため、パフォーマンス上の問題はほとんどありません。Options APIコンポーネントでのストアの使用は、Setup APIに比べて記述が少し冗長になる傾向があります。
7. 実践的なPiniaの使い方
実際のアプリケーション開発でよく遭遇するシナリオにおけるPiniaの使い方を見ていきましょう。
7.1 モジュール分割(複数のストアを持つ)
Piniaは、最初からストアがモジュール化されています。各defineStore
の呼び出しが独立したストア(モジュール)を作成します。アプリケーションの規模が大きくなったら、機能やドメインごとにストアを分割するのが良いプラクティスです。
例:認証ストア、商品ストア、カートストアなど。
```javascript
// src/stores/auth.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useAuthStore = defineStore('auth', () => {
const user = ref(null);
const isAuthenticated = computed(() => user.value !== null);
async function login(credentials) {
// API呼び出しをシミュレート
await new Promise(resolve => setTimeout(resolve, 500));
user.value = { name: credentials.username }; // ダミーユーザーを設定
}
function logout() {
user.value = null;
}
return {
user,
isAuthenticated,
login,
logout
};
});
```
```javascript
// src/stores/products.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useProductsStore = defineStore('products', () => {
const items = ref([]);
const isLoading = ref(false);
async function fetchProducts() {
isLoading.value = true;
// API呼び出しをシミュレート
await new Promise(resolve => setTimeout(resolve, 1000));
items.value = [{ id: 1, name: 'Product A', price: 10 }, { id: 2, name: 'Product B', price: 20 }]; // ダミーデータ
isLoading.value = false;
}
return {
items,
isLoading,
fetchProducts
};
});
```
コンポーネントでは、必要なストアだけをインポートして使用します。
```vue
ようこそ、{{ user.name }}さん
ログインしていません
商品リスト
商品情報を読み込み中...
- {{ item.name }} - ¥{{ item.price }}
```
このように、複数のストアを定義し、必要なコンポーネントでそれぞれをインポートして使用することで、コードを整理し、関心の分離を明確に保つことができます。
7.2 ストア間の連携
一つのストアのアクション内で、別のストアのアクションを呼び出したり、別のストアの状態やゲッターを参照したりすることが可能です。
例:ログイン成功後に、ユーザー固有の設定を別のストアから読み込む。
```javascript
// src/stores/settings.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useSettingsStore = defineStore('settings', () => {
const theme = ref('light');
async function fetchSettingsForUser(userId) {
console.log(Fetching settings for user: ${userId}
);
// API呼び出しをシミュレート
await new Promise(resolve => setTimeout(resolve, 300));
theme.value = userId === 1 ? 'dark' : 'light'; // ダミー設定
}
function setTheme(newTheme) {
theme.value = newTheme;
}
return {
theme,
fetchSettingsForUser,
setTheme
};
});
```
```javascript
// src/stores/auth.js (修正版)
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { useSettingsStore } from './settings'; // 別のストアをインポート
export const useAuthStore = defineStore('auth', () => {
const user = ref(null);
const isAuthenticated = computed(() => user.value !== null);
async function login(credentials) {
// API呼び出しをシミュレート
await new Promise(resolve => setTimeout(resolve, 500));
user.value = { id: 1, name: credentials.username }; // ダミーユーザーを設定 (IDを追加)
// ログイン成功後、Settingsストアのアクションを呼び出す
const settingsStore = useSettingsStore(); // アクション内でストアインスタンスを取得
await settingsStore.fetchSettingsForUser(user.value.id);
console.log('Login successful and settings fetched.');
}
function logout() {
user.value = null;
// ログアウト時にSettingsストアをリセットすることも可能
const settingsStore = useSettingsStore();
settingsStore.$reset(); // Piniaが提供する便利なストアリセットメソッド
}
return {
user,
isAuthenticated,
login,
logout
};
});
```
この例では、auth
ストアのlogin
アクション内でuseSettingsStore()
を呼び出し、settings
ストアのインスタンスを取得しています。そして、そのインスタンスを通じてfetchSettingsForUser
アクションを呼び出しています。Setup APIスタイルでは、他のストアのStateやGetterを参照する場合も同様に、参照元のストアインスタンスを取得してからアクセスします。
Options APIスタイルのストアでストア間連携を行う場合も、同様にアクションやゲッター内でuseOtherStore()
を呼び出してインスタンスを取得します。
7.3 非同期処理(アクション内でのAPI呼び出し)
API呼び出しやデータベース操作などの非同期処理は、Piniaではアクション内で行うのが標準的な方法です。アクションはPromiseを返すことができるため、非同期処理の結果を待つことも容易です。
先ほどのfetchProducts
やlogin
アクションの例のように、async
/await
構文をそのまま使用できます。非同期処理の開始時と完了時にStateを更新することで、ローディング状態などをコンポーネントに伝えることができます。
javascript
// 非同期アクションの例
actions: {
async fetchUsers() {
this.isLoading = true; // ローディング状態をtrueにする
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const data = await response.json();
this.users = data; // 取得したデータでStateを更新
} catch (error) {
console.error('Error fetching users:', error);
// エラー状態をStateに保存することも可能
// this.error = error.message;
} finally {
this.isLoading = false; // ローディング状態をfalseにする
}
}
}
Setup APIスタイルでも同様です。
```javascript
// Setup APIスタイルでの非同期アクションの例
const isLoading = ref(false);
const users = ref([]);
async function fetchUsers() {
isLoading.value = true;
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const data = await response.json();
users.value = data;
} catch (error) {
console.error('Error fetching users:', error);
} finally {
isLoading.value = false;
}
}
return { users, isLoading, fetchUsers };
```
7.4 プラグイン (Plugins)
Piniaは、ストアの機能を拡張するための強力なプラグインシステムを持っています。Piniaプラグインは、ストアの作成時に追加のプロパティやメソッドを追加したり、アクションの実行を傍受したり、ローカルストレージへの永続化のようなグローバルな副作用を実行したりできます。
Piniaプラグインは関数として定義し、createPinia()
に渡すことで登録します。プラグイン関数は、引数としてPiniaのコンテキストオブジェクトを受け取ります。このオブジェクトには、現在のPiniaインスタンス、アプリ、ストアのインスタンスなどが含まれます。
``javascript
新しいストアが作成されました: ${store.$id}`);
// src/plugins/myPlugin.js
export function myPiniaPlugin({ pinia, app, store, options }) {
// store インスタンスが作成されるたびに実行される
console.log(
// ストアに新しいプロパティを追加する
store.myProperty = 'Hello from plugin!';
// ストアに新しいアクションを追加する
store.myAction = () => {
console.log(Plugin action called for ${store.$id}
);
// このアクション内でストアの状態や他のアクションにアクセス可能
// store.someState = 'updated by plugin';
};
// ストアのアクションをフックする
store.$onAction(({
name, // actionの名前
store, // store インスタンス
args, // actionに渡された引数
after, // Promiseを返す関数。actionが成功した後に呼ばれる
onError, // Promiseを返す関数。actionが失敗した後に呼ばれる
}) => {
const startTime = Date.now();
console.log(Action "${name}" starting in store "${store.$id}" with args ${JSON.stringify(args)}.
);
// actionが完了した後に呼ばれる
after((result) => {
console.log(`Action "${name}" finished in store "${store.$id}" after ${Date.now() - startTime}ms. Result: ${JSON.stringify(result)}.`);
});
// actionがエラーを投げた後に呼ばれる
onError((error) => {
console.error(`Action "${name}" failed in store "${store.$id}" after ${Date.now() - startTime}ms. Error:`, error);
});
});
}
```
このプラグインを登録するには、main.js
/main.ts
でcreatePinia()
に渡します。
```javascript
// src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import { myPiniaPlugin } from './plugins/myPlugin' // 作成したプラグインをインポート
const pinia = createPinia()
// プラグインを登録
pinia.use(myPiniaPlugin)
const app = createApp(App)
app.use(pinia)
app.mount('#app')
```
登録されたプラグインは、アプリケーションのPiniaインスタンスに適用され、その後に作成されるすべてのストアインスタンスに対して実行されます。
例:localStorageへの永続化プラグイン
Piniaプラグインの一般的なユースケースとして、特定のストアの状態をブラウザのlocalStorageに自動的に保存し、アプリケーション起動時に読み込む、という永続化機能があります。公式ドキュメントでもプラグインを使った例が紹介されています。
```javascript
// src/plugins/persistedState.js
import { toRaw } from 'vue'; // リアクティブProxyを通常のオブジェクトに戻す
// ストア定義オプションに 'persist: true' を持つストアを対象とするプラグイン
export function persistedStatePlugin({ store, options }) {
// options.persist が true の場合のみ処理を行う
if (options.persist) {
const storageKey = pinia:${store.$id}
; // localStorageのキー
// アプリケーション起動時: localStorage から状態を読み込む
const savedState = localStorage.getItem(storageKey);
if (savedState) {
try {
// JSONをパースしてストアの状態に適用
store.$patch(JSON.parse(savedState));
} catch (e) {
console.error(`Failed to load state for store "${store.$id}" from localStorage`, e);
localStorage.removeItem(storageKey); // 読み込み失敗した場合は削除
}
}
// 状態が変更されるたびに localStorage に保存する
store.$subscribe((mutation, state) => {
// mutation.type は 'direct', 'patch object', 'patch function' のいずれか
// mutation.storeId は ストアID
// state は変更後の新しい状態
try {
// 状態全体をJSON文字列に変換して保存
// toRaw を使うことで、Proxyオブジェクトではなく元のオブジェクトをシリアライズする
localStorage.setItem(storageKey, JSON.stringify(toRaw(state)));
} catch (e) {
console.error(`Failed to save state for store "${store.$id}" to localStorage`, e);
}
}, { detached: true }); // コンポーネントがアンマウントされても購読を継続
}
}
```
このプラグインを使うには、main.js
/main.ts
で登録し、永続化したいストアの定義にpersist: true
というカスタムオプションを追加します(これはPiniaのコア機能ではなく、プラグイン側で解釈するオプションです)。Options APIスタイル、Setup APIスタイルどちらでもdefineStore
の第2引数に直接プロパティとして追加できます。
```javascript
// src/main.js (プラグイン登録)
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import { persistedStatePlugin } from './plugins/persistedState'
const pinia = createPinia()
pinia.use(persistedStatePlugin) // 永続化プラグインを登録
const app = createApp(App)
app.use(pinia)
app.mount('#app')
```
```javascript
// src/stores/userSettings.js (永続化したいストアの例)
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useUserSettingsStore = defineStore('userSettings', {
// Options APIスタイル
state: () => ({
theme: 'light',
language: 'en'
}),
actions: {
setTheme(theme) { this.theme = theme; },
setLanguage(lang) { this.language = lang; }
},
// ↓ プラグインが読み取るカスタムオプション
persist: true
});
// または Setup APIスタイル
// export const useUserSettingsStore = defineStore('userSettings', () => {
// const theme = ref('light');
// const language = ref('en');
//
// function setTheme(theme) { theme.value = theme; }
// function setLanguage(lang) { language.value = lang; }
//
// return {
// theme,
// language,
// setTheme,
// setLanguage,
// // persist: true // Setup APIスタイルでは、戻り値のオブジェクトとは別にオプションを定義
// };
// }, {
// // ↓ Setup APIスタイルでは、第3引数にオプションを渡す
// persist: true
// });
``
persist
PiniaのSetup APIスタイルでストアオプション(のようなカスタムオプションを含む)を指定する場合は、
defineStore`の第3引数にオブジェクトとして渡します。
このように、Piniaプラグインを使うことで、様々な共通処理や拡張機能をストアに簡単に追加できます。永続化プラグイン以外にも、ロギング、エラーハンドリング、特定のデータ変換など、応用範囲は多岐にわたります。
7.5 テスト (Testing)
Piniaストアは、単なるJavaScriptオブジェクトと関数として定義されているため、単体テストが非常に容易です。ストアは、VueコンポーネントやVueインスタンスに依存せずにテストできます。
ストアをテストする一般的な手順は以下の通りです。
- テスト対象のストアをインポートします。
- テスト用のPiniaインスタンスを作成し、そのインスタンスのコンテキスト内でストアを使用します。
createPinia()
は引数なしで呼び出せます。 - テストケースごとに、ストアの状態を初期化します。
store.$reset()
メソッドを使うと、ストア定義のstate
関数(Options API)またはSetup関数(Setup API)で定義された初期状態にリセットできます。 - テストしたいState、Getter、Actionにアクセスし、期待通りの動作をするかアサーションを行います。
例:カウンターストアのテスト(JestやVitestを想定)
```javascript
// src/stores/tests/counter.spec.js (または .ts)
import { createPinia, setActivePinia } from 'pinia';
import { useCounterStore } from '../counter'; // テスト対象のストア
describe('Counter Store', () => {
beforeEach(() => {
// 各テストの前に新しいPiniaインスタンスを作成し、アクティブにする
// これにより、ストアインスタンスが各テストで独立したものになる
setActivePinia(createPinia());
});
it('should initialize with count 0', () => {
const counter = useCounterStore();
expect(counter.count).toBe(0);
});
it('should increment the count', () => {
const counter = useCounterStore();
counter.increment();
expect(counter.count).toBe(1);
});
it('should decrement the count', () => {
const counter = useCounterStore();
counter.count = 5; // 初期状態を直接設定(テスト時のみ)
counter.decrement();
expect(counter.count).toBe(4);
});
it('should increment the count by amount', () => {
const counter = useCounterStore();
counter.incrementBy(10);
expect(counter.count).toBe(10);
});
it('should double the count', () => {
const counter = useCounterStore();
counter.count = 3;
expect(counter.doubleCount).toBe(6);
});
it('should increment the count asynchronously', async () => {
const counter = useCounterStore();
// 非同期アクションを await する
await counter.incrementAsync();
expect(counter.count).toBe(1);
});
// $reset メソッドのテスト
it('should reset the store state', () => {
const counter = useCounterStore();
counter.count = 100;
expect(counter.count).toBe(100);
counter.$reset();
expect(counter.count).toBe(0); // 初期値に戻る
});
});
```
setActivePinia(createPinia())
は、現在のテストコンテキストでPiniaをアクティブにするための重要なステップです。これにより、useStore()
が正しく動作し、各テストケースが独立したストアインスタンスを持つことを保証します。
非同期アクションのテストも、async
/await
を使って容易に記述できます。
Piniaストアの単体テストは、状態管理ロジックの正しさを保証する上で非常に効果的です。
8. Piniaの高度なトピック
8.1 PiniaとTypeScript (TypeScript Integration)
PiniaはTypeScriptで書かれており、TypeScriptとの親和性が非常に高いです。これにより、状態管理コードに型安全性がもたらされ、開発効率とコードの堅牢性が向上します。
Piniaが提供する主なTypeScriptのメリットは以下の通りです。
- 強力な型推論: ストアを定義し、コンポーネントで使用する際に、State, Getters, Actionsの型が自動的に推論されます。タイプミスや誤った引数の使用などが開発段階で検出されます。
- 明示的な型定義: 複雑なStateの構造やActionの引数/戻り値に対して、インターフェースや型エイリアスを使って明示的に型を定義できます。
- 補完機能: エディタの補完機能が強力に働くため、ストアのプロパティやメソッド名を正確に入力しやすくなります。
Options APIスタイルでの型付け:
Options APIスタイルでは、defineStore
のジェネリクス引数としてStateの型を渡すことで、状態の型付けができます。GettersやActionsのthis
を使用する場合、適切な型アノテーションが必要です。
```typescript
// src/stores/counter.ts (Options API + TypeScript)
import { defineStore } from 'pinia'
interface CounterState {
count: number
// 複雑なオブジェクトなどもここに定義
settings: {
theme: string
fontSize: number
}
}
export const useCounterStore = defineStore<"counter", CounterState>('counter', { // ① ジェネリクスでストアIDとState型を指定
state: () => ({ // state 関数は CounterState 型のオブジェクトを返す
count: 0,
settings: {
theme: 'light',
fontSize: 14
}
}),
getters: {
doubleCount: (state: CounterState): number => state.count * 2, // ② 引数と戻り値の型付け
doubleCountPlusOne(): number { // ③ this を使うゲッターは戻り値の型付け
return this.doubleCount + 1;
},
// ネストされたStateへのアクセスも型安全
currentTheme(): string {
return this.settings.theme;
}
},
actions: {
increment() { // 戻り値が void のアクションは型付け不要(通常)
this.count++;
},
incrementBy(amount: number) { // ④ 引数の型付け
this.count += amount;
},
setTheme(theme: 'light' | 'dark') { // ⑤ ユニオン型での型付け
this.settings.theme = theme;
},
async incrementAsync(): Promise
await new Promise(resolve => setTimeout(resolve, 1000));
this.count++;
}
},
});
``
defineStore
①の最初のジェネリクス引数にストアIDの型(通常は文字列リテラル型)、2つ目のジェネリクス引数にStateの型を指定するのが推奨される型付け方法です。
this
② Stateを引数として受け取るゲッターは、引数と戻り値の型を明示できます。
③を使って他のゲッターやStateにアクセスするゲッターは、戻り値の型を明示する必要があります。
Promise
④ Actionの引数には適切な型を付けます。
⑤ ⑥ 非同期アクションなどでPromiseを返す場合は、戻り値の型を
Setup APIスタイルでの型付け:
Setup APIスタイルは、Vue 3のComposition APIと同様に、ローカル変数や関数に直接型を付けることで型安全性を確保します。Options APIスタイルと比べて、型推論がより強力に働く傾向があり、記述量も少なく済むことが多いです。
```typescript
// src/stores/counter.ts (Setup API + TypeScript)
import { defineStore } from 'pinia';
import { ref, computed, reactive } from 'vue';
interface SettingsState {
theme: 'light' | 'dark';
fontSize: number;
}
export const useCounterStore = defineStore('counter', () => {
// State: ref や reactive のジェネリクスで型指定
const count = ref
const settings = reactive
theme: 'light',
fontSize: 14
});
// Getters: computed のジェネリクスで戻り値型指定 (多くの場合推論可能)
const doubleCount = computed
const doubleCountPlusOne = computed
const currentTheme = computed
// Actions: 関数の引数と戻り値型指定
function increment(): void { // 戻り値 void は省略可能
count.value++;
}
function incrementBy(amount: number): void {
count.value += amount;
}
function setTheme(theme: SettingsState['theme']): void { // 型エイリアスやユニオン型で指定
settings.theme = theme;
}
async function incrementAsync(): Promise
await new Promise(resolve => setTimeout(resolve, 1000));
count.value++;
}
// 返すオブジェクトの型は自動で推論されるため、通常明示的な型付けは不要
return {
count,
settings,
doubleCount,
doubleCountPlusOne,
currentTheme,
increment,
incrementBy,
setTheme,
incrementAsync,
};
});
``
ref
Setup APIスタイルでは、や
reactiveのジェネリクス、関数の引数や戻り値に型を付けることで、ストア全体の型安全性を確保できます。
computed`の戻り値型は多くの場合推論できますが、明示的に指定しても構いません。
どちらのスタイルでも、PiniaはTypeScriptと非常によく連携します。TypeScriptプロジェクトでPiniaを使用することは強く推奨されます。
8.2 Pinia Devtools
Vue DevtoolsにはPinia専用のパネルがあり、ストアの状態を視覚的に確認したり、状態変更の履歴を追跡したり、アクションの実行や状態の変更を手動で行ったりすることができます。これはデバッグにおいて非常に強力なツールとなります。
Vue Devtoolsをブラウザ(Chrome/Firefox)にインストールし、Vue 3アプリケーションでPiniaを使用していれば、通常は自動的にPiniaパネルが表示されます。
Pinia Devtoolsでできること:
- ストアの一覧表示: アプリケーションに存在するすべてのPiniaストアが表示されます。
- 状態の確認と編集: 各ストアの現在の状態(State)を確認できます。開発モードでは、Stateの値をインラインで編集して、UIがどのように変化するかを即座に確認できます。
- ゲッターの確認: 各ストアのゲッターの現在の値を確認できます。
- タイムトラベルデバッグ: Actionや
$patch
による状態変更の履歴が表示されます。履歴を選択することで、その時点の状態にアプリケーションを巻き戻す「タイムトラベル」が可能です(ただし、非同期アクションや外部の状態(URLなど)に依存するアクションには注意が必要です)。 - アクションの実行: Devtoolsから直接ストアのアクションを呼び出すことができます。引数を指定してアクションを実行し、その結果を確認できます。
- 状態変更のフック:
$subscribe
による状態変更の監視もDevtools上で視覚的に確認できます。
これらの機能は、状態管理に関連するバグを特定し、デバッグプロセスを大幅に効率化します。Piniaを使用する際は、必ずVue Devtoolsを併用することをお勧めします。
9. PiniaとVuexの比較
PiniaとVuexはどちらもVue.jsのための状態管理ライブラリですが、設計思想やAPIに違いがあります。PiniaはVuex v4(Vue 3対応版)よりもさらにVue 3のComposition APIに最適化され、シンプルになっています。
機能/概念 | Vuex (v4) | Pinia | 備考 |
---|---|---|---|
State (状態) | Options (state プロパティ) |
Options (state 関数) または Setup (ref /reactive ) |
Piniaの方が柔軟 |
Getters (算出) | Options (getters プロパティ) |
Options (getters プロパティ) または Setup (computed ) |
Pinia SetupスタイルはComposition APIに類似 |
Mutations (同期変更) | あり (必須) | なし | PiniaはActionで直接変更。概念が減りシンプルに |
Actions (非同期/ロジック) | あり (Mutationをコミット) | あり (Stateを直接変更) | PiniaはMutation不要でシンプル |
モジュール分割 | Namespaced Modules (名前空間付き) | 最初からモジュール化 (IDで区別) | Piniaは名前空間のネストがなくシンプル |
TypeScriptサポート | v4で改善されたが、型定義がやや複雑になりがち | 最初からTypeScriptで開発、非常に強力な型推論 | Piniaの方が優れている |
シンプルさ | やや概念が多い (State, Getters, Mutations, Actions) | シンプル (State, Getters, Actions) | Piniaの方が学習しやすい |
学習コスト | やや高い | 低い | Piniaの方が導入・理解しやすい |
バンドルサイズ | やや大きい | 軽量 | Piniaの方が小さい |
Vue Devtools | サポートあり (Vuexタブ) | サポートあり (Piniaタブ) | どちらも強力なデバッグ機能 |
Vue 2対応 | v3まで公式、v4はプラグインで対応 | v1まで公式、v2からはVue 3のみ | 新規はPinia + Vue 3 が推奨。既存Vue 2はVuex v3など |
Piniaが優れている点:
- シンプルで学習しやすい: Mutationがないこと、モジュール化が直感的なことなどから、Vuexに比べて概念が少なく、初心者でも理解しやすいです。
- TypeScriptとの親和性: 強力な型推論により、TypeScriptでの開発体験が非常に優れています。
- 軽量: バンドルサイズが小さく、パフォーマンスに貢献します。
- Vue 3 Composition APIとの連携: Setup APIスタイルはComposition APIの利用者に自然に馴染みます。
- 自動的なモジュール化: 名前空間を意識することなく、ストアを分割できます。
Vuexを使うべきケース:
- 既存のVuexアプリケーションをそのまま維持・開発する場合。
- Vue 2環境での開発(Pinia v1系を使うか、Vuex v3など)。ただし、Pinia v1系はメンテナンスモードに入っているため、Vue 3への移行を検討すべきです。
- VuexのMutationによる厳格な状態変更フローにメリットを感じる場合(ただしPiniaでもDevtoolsでActionsを追跡可能)。
新規のVue 3アプリケーション開発においては、PiniaがVuexに代わる推奨の状態管理ライブラリとなっています。PiniaはVuexの多くの課題を解決し、よりモダンで効率的な状態管理を提供します。
VuexからPiniaへの移行も比較的容易です。VuexのState, Getters, ActionsはPiniaでも対応する概念があるため、コードを書き換える際のMappingがしやすいです。Mutation内のロジックは、対応するActionにそのまま移動できます。
10. まとめ
Piniaは、Vue.jsのための現代的でシンプル、かつ強力な状態管理ライブラリです。Vue 3のComposition APIと密接に連携するように設計されており、Vuexが持ついくつかの課題を克服しています。
本記事では、Piniaの以下の点について詳しく解説しました。
- なぜアプリケーションの状態管理が必要なのか、そして状態管理ライブラリがどのようにその課題を解決するのか。
- PiniaがVuexの後継として登場した背景とその主な特徴・利点(シンプルさ、TypeScriptサポート、軽量性など)。
- Piniaの核となる概念であるState, Getters, Actions。
- Piniaのプロジェクトへの導入方法(インストール、プラグイン登録)。
defineStore
を使ったストアの定義方法(Options APIスタイルとSetup APIスタイル)とその詳細。- コンポーネントで
useStore
やstoreToRefs
を使ってストアの状態やアクションを利用する方法。 - 複数のストアを使ったモジュール分割、ストア間の連携、非同期処理の扱い方。
- Piniaプラグインによるストアの拡張(永続化プラグインの例)。
- Piniaストアの単体テスト方法。
- TypeScriptを最大限に活用した型安全な状態管理。
- Vue Devtoolsを使ったデバッグ方法。
- PiniaとVuexの比較。
Piniaを導入することで、Vue.jsアプリケーションの状態管理はよりシンプルに、より見通しよく、そしてより堅牢になります。特にVue 3とComposition APIを使用しているプロジェクトでは、Piniaの強力な機能と開発体験の向上を実感できるでしょう。
小規模なアプリケーションではPropsやProvide/Injectで十分な場合もありますが、アプリケーションの規模が少しでも大きくなる可能性がある場合や、複数のコンポーネントで同じ状態を共有する必要が出てきた場合は、早い段階でPiniaの導入を検討することをお勧めします。
本記事が、Piniaを使ったVue.jsの状態管理を学び、実践する上での手助けとなれば幸いです。Piniaの公式ドキュメントも非常に充実しているので、さらに深く学びたい場合はそちらも参照してみてください。Piniaを使いこなして、効率的で保守しやすいVue.jsアプリケーション開発を進めましょう!