Piniaとは?Vueの新しい状態管理ライブラリを徹底解説
はじめに:なぜ状態管理が必要なのか?
Vue.jsを使ったアプリケーション開発において、コンポーネント間でデータを共有したり、アプリケーション全体で管理すべき状態(ログイン情報、テーマ設定、ショッピングカートの中身など)を扱うことは避けられません。小規模なアプリケーションであれば、Propsによるデータの受け渡しや、イベントによる通知で対応できるかもしれません。しかし、アプリケーションが複雑化し、コンポーネントツリーが深くなるにつれて、データの受け渡し(Props Drilling)やイベントの管理は非常に煩雑になります。どのコンポーネントがどのデータを持ち、どのコンポーネントに渡すのか、データがどこで更新されるのかを追跡するのが困難になり、「状態管理」の問題が発生します。
このような問題を解決するために登場するのが「状態管理ライブラリ」です。状態管理ライブラリは、アプリケーション全体で共有される状態を一箇所に集中させ、その状態へのアクセスや更新を予測可能な方法で行えるようにするためのパターンやツールを提供します。Vue.jsの世界では、これまでVuexがその役割を担ってきました。
そして、Vue 3の時代になり、Composition APIの登場やTypeScriptとの親和性の向上といった変化に合わせて、Vuexの新しい世代とも言える状態管理ライブラリが登場しました。それが「Pinia」です。
本記事では、Piniaがどのようなライブラリなのか、なぜVuexに代わる存在として推奨されているのか、その基本的な使い方から応用的な機能、Vuexとの違いまでを徹底的に解説します。これを読めば、PiniaをあなたのVue.js開発に取り入れるための知識が網羅的に身につくはずです。
Piniaとは何か? Vuexの進化系
Piniaは、Vue.jsのための公式な状態管理ライブラリです。Vueのコアチームのメンバーによって開発されており、Vue 3の公式な推奨状態管理ライブラリとして位置づけられています。
その起源は、Vuexの次期バージョンであるVuex 5の実験的な開発から派生したものです。Vuex 5で導入が検討されていた多くのアイデアや改善点が、Piniaとして先行して実装され、その使いやすさや設計の洗練さから、Vuex 4よりもPiniaを先行して公式推奨とすることが決定されました。
Piniaの名前は、スペイン語でパイナップルを意味する “piña” から来ており、これは複数のファイル(store)が組み合わさって大きなストアを形成する様子を、パイナップルの房が集まっている様子に見立てたものです。
PiniaはVue 2でも使用可能ですが、Vue 3との親和性が高く、特にComposition APIやTypeScriptをフル活用したい開発者にとって非常に魅力的な選択肢となっています。
なぜPiniaを使うべきなのか? Vuexからの改善点
Vuexも素晴らしい状態管理ライブラリですが、PiniaはVuexの経験を踏まえ、いくつかの重要な改善が施されています。Piniaを使うべき主な理由は以下の通りです。
-
シンンプルなAPIとより直感的なコード構造:
- Vuexでは、State, Getters, Mutations, Actions, Modulesといった概念があり、特にMutations(状態を同期的に変更するための唯一の方法)は多くの初心者を混乱させるポイントでした。PiniaではMutationsの概念がなくなり、Actionsですべての状態変更(同期/非同期問わず)を行います。これにより、覚えるべき概念が減り、コードがシンプルになります。
- ストアの定義がよりフラットで書きやすくなりました。特にComposition APIスタイルの定義は、Vue 3の開発体験と自然に調和します。
-
モジュール性がデフォルト:
- Piniaのストアは、デフォルトで名前空間化(namespaced)されています。Vuexではモジュールを名前空間化するかどうかを選択する必要がありましたが、Piniaでは各ストアが独自のIDを持つため、常に独立しており、名前の衝突を心配する必要がありません。ストアをインポートして使うだけで、そのストア内のState, Getters, Actionsにアクセスできます。
-
優れたTypeScriptサポート:
- Piniaは最初からTypeScriptフレンドリーに設計されています。VuexでもTypeScriptは使えましたが、型推論が限定的だったり、複雑な型定義が必要な場面がありました。Piniaでは、定義したストアの型が強力に推論されるため、State, Getters, Actionsにアクセスする際に補完が効き、開発効率とコードの安全性が大幅に向上します。特にComposition APIスタイルのストア定義では、型推論が非常に強力です。
-
軽量でパフォーマンスが良い:
- PiniaはVuexと比較して非常に軽量です。また、Vue 3のリアクティビティシステムを最大限に活用しており、パフォーマンスにも優れています。
- デフォルトのモジュール設計により、アプリケーションが必要とするストアだけをバンドルに含めることが容易になり、より効率的なツリーシェイキングが可能です。
-
素晴らしいDevtools統合:
- Vue Devtoolsとの連携が非常に強力です。Vuexと同様に、ストアの状態の確認、変更の追跡(タイムトラベルデバッグ)、パフォーマンスプロファイリングなどが可能です。特に、PiniaのDevtoolsはVuexのそれよりも洗練されており、複数のストアの状態を簡単に切り替えたり、変更履歴をより見やすく確認したりできます。
-
Mutationが存在しない:
- VuexのMutationは、状態を同期的に変更するための制約であり、非同期処理はActionsで行い、その中でMutationをコミットするという二段階が必要でした。PiniaではこのMutationのステップが不要です。Actions内で直接状態を変更できます(もちろん、非同期処理もActionsで行います)。これにより、コードの記述量が減り、概念的な複雑さも軽減されます。
これらの理由から、特にVue 3で新規にアプリケーションを開発する場合や、VuexのアプリケーションをVue 3に移行する際には、Piniaが強く推奨されます。
Piniaのコアコンセプト:State, Getters, Actions
Piniaを理解するための核となる概念は、Vuexと同様にState, Getters, Actionsの3つです。ただし、その定義方法や役割にはPiniaならではの特徴があります。
1. State(状態)
- 役割: アプリケーションのあらゆる場所からアクセスできる、集中管理されたデータです。VuexのStateと同じように、単一の真実の源(Single Source of Truth)として機能します。
- 定義方法: ストアを定義する際に、Stateを返す関数として定義します。これは、サーバーサイドレンダリング(SSR)時に各リクエストに対して独立したStateインスタンスを提供するために重要です。
- 特徴:
- PiniaのStateは、Vue 3のリアクティビティシステム(
ref
やreactive
)の上に構築されています。つまり、Stateの変更は自動的にそれを使用しているコンポーネントに反映されます。 - VuexのようにStateを直接Mutationで変更する必要はなく、Actions内で直接変更できます。
- PiniaのStateは、Vue 3のリアクティビティシステム(
コード例:
“`javascript
import { defineStore } from ‘pinia’;
export const useCounterStore = defineStore(‘counter’, {
state: () => ({
count: 0,
name: ‘Eduardo’,
}),
});
“`
この例では、count
とname
という2つのプロパティを持つStateを定義しています。defineStore
の第一引数 'counter'
は、このストアのユニークなIDです。これはDevtoolsでの識別などに使われます。
2. Getters(ゲッター)
- 役割: Stateから派生した状態を算出するプロパティです。VuexのGettersと同じように、Stateのデータに基づいて計算された値を参照できます。Stateが変更されると、Gettersも自動的に再計算されます。
- 定義方法: ストア定義の一部としてメソッドとして定義します。これらのメソッドは、
state
オブジェクトを第一引数として受け取ります。他のGettersにアクセスしたい場合は、第二引数としてgetters
オブジェクトを受け取ることも可能です(Options APIスタイル)。Composition APIスタイルでは、Computed Propertiesとして定義します。 - 特徴:
- PiniaのGettersは、VueのComputed Propertiesと同様にキャッシュされます。Stateが変更されない限り、同じGettersを複数回参照しても再計算は行われません。
- GettersはStateを参照するためのものであり、Getters内でStateを直接変更すべきではありません。
コード例 (Options APIスタイル):
“`javascript
import { defineStore } from ‘pinia’;
export const useCounterStore = defineStore(‘counter’, {
state: () => ({
count: 0,
name: ‘Eduardo’,
}),
getters: {
// Stateにアクセスするシンプルなゲッター
doubleCount: (state) => state.count * 2,
// 他のゲッターにアクセスするゲッター
// `this` を使う場合はアロー関数ではなく通常の関数で定義
doubleCountPlusOne(): number {
return this.doubleCount + 1;
},
// 引数を受け取るゲッター (キャッシュはされない)
getCounterPlus(amount: number) {
return (state) => state.count + amount;
}
},
});
“`
コード例 (Composition APIスタイル):
“`javascript
import { defineStore } from ‘pinia’;
import { ref, computed } from ‘vue’;
export const useCounterStore = defineStore(‘counter’, () => {
const count = ref(0);
const name = ref(‘Eduardo’);
const doubleCount = computed(() => count.value * 2);
// 他のゲッター (computed) にアクセス
const doubleCountPlusOne = computed(() => doubleCount.value + 1);
// 引数を受け取る関数を返すゲッター
const getCounterPlus = (amount: number) => {
return computed(() => count.value + amount); // または単に関数を返すだけでもOK
};
return {
count,
name,
doubleCount,
doubleCountPlusOne,
getCounterPlus, // 関数として公開
};
});
``
computed`プロパティとして定義されます。引数を受け取りたい場合は、ゲッターとして振る舞う関数を返します。
Composition APIスタイルでは、Gettersは単に
3. Actions(アクション)
- 役割: Stateを変更するためのロジックを記述する場所です。同期処理、非同期処理のどちらもActionsで行います。VuexのActionsとMutationsの両方の役割をPiniaのActionsが担います。
- 定義方法: ストア定義の一部としてメソッドとして定義します。これらのメソッドは、
this
キーワード(Options APIスタイル)または返されたオブジェクト内のリアクティブ変数(Composition APIスタイル)を介して、同じストア内のStateやGetters、他のActionsにアクセスできます。 - 特徴:
- Actions内でStateを直接変更できます。VuexのようにMutationをコミットする必要はありません。
async/await
を使って非同期処理を簡単に扱えます。API呼び出しなどの副作用はActions内で実行するのが一般的です。- 他のストアのActionsを呼び出すことも可能です。
コード例 (Options APIスタイル):
“`javascript
import { defineStore } from ‘pinia’;
import { useOtherStore } from ‘./otherStore’; // 他のストアをインポート
export const useCounterStore = defineStore(‘counter’, {
state: () => ({
count: 0,
name: ‘Eduardo’,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
// 同期アクション
increment(amount = 1) {
this.count += amount; // Stateを直接変更
},
// 非同期アクション
async incrementAsync(amount = 1) {
await new Promise((resolve) => setTimeout(resolve, 1000));
this.count += amount; // 非同期処理後もStateを直接変更
},
// 他のストアのアクションを呼び出す
callOtherAction() {
const otherStore = useOtherStore(); // 他のストアのインスタンスを取得
otherStore.someAction();
},
},
});
“`
コード例 (Composition APIスタイル):
“`javascript
import { defineStore } from ‘pinia’;
import { ref, computed } from ‘vue’;
import { useOtherStore } from ‘./otherStore’; // 他のストアをインポート
export const useCounterStore = defineStore(‘counter’, () => {
const count = ref(0);
const name = ref(‘Eduardo’);
const doubleCount = computed(() => count.value * 2);
// 同期アクション
const increment = (amount = 1) => {
count.value += amount; // State (ref) の .value を変更
};
// 非同期アクション
const incrementAsync = async (amount = 1) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
count.value += amount;
};
// 他のストアのアクションを呼び出す
const callOtherAction = () => {
const otherStore = useOtherStore(); // 他のストアのインスタンスを取得
otherStore.someAction();
};
return {
count,
name,
doubleCount,
increment,
incrementAsync,
callOtherAction,
};
});
“`
ActionsはVue Devtoolsで追跡可能であり、どのActionsがいつ実行されたか、どのような引数が渡されたかを確認できます。
Piniaの基本的な使い方
Piniaを使うためのステップは非常に簡単です。
1. インストール
npmまたはyarnを使ってPiniaをプロジェクトにインストールします。
“`bash
npm install pinia
あるいは
yarn add pinia
“`
2. Piniaインスタンスの作成とVueアプリへの登録
Vueアプリケーションのエントリーポイント(通常は main.js
または main.ts
)でPiniaインスタンスを作成し、Vueアプリに渡します。
“`javascript
// main.js (または main.ts)
import { createApp } from ‘vue’;
import { createPinia } from ‘pinia’;
import App from ‘./App.vue’;
const app = createApp(App);
const pinia = createPinia(); // Piniaインスタンスを作成
app.use(pinia); // VueアプリにPiniaを登録
app.mount(‘#app’);
“`
これで、アプリケーション全体でPiniaストアを使用する準備が整いました。
3. ストアの定義
前述のコード例のように、src/stores
ディレクトリなどに各ストアのファイルを配置し、defineStore
を使ってストアを定義します。
例えば、src/stores/counter.js
(または counter.ts
) に以下のように記述します。
“`javascript
// src/stores/counter.js
import { defineStore } from ‘pinia’;
export const useCounterStore = defineStore(‘counter’, {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++;
},
},
});
“`
ストアのID ('counter'
) はアプリケーション全体でユニークである必要があります。慣習として、ファイル名と同じIDを使用することが多いです。また、ストアを返す関数名も慣習として use
+ ストア名 + Store
(useCounterStore
) とすることが推奨されています。
4. コンポーネントでのストアの使用
コンポーネント内でストアを使用するには、まず定義したストアをインポートし、そのストアを返す関数(例: useCounterStore
)を呼び出します。この関数はコンポーネントの setup()
関数内(Composition API)またはそれと同等のコンテキスト(Options APIの算出プロパティやメソッド内など)で呼び出す必要があります。
Composition APIで使う場合:
“`vue
Count: {{ counter.count }}
Double Count: {{ counter.doubleCount }}
“`
StateやGettersをテンプレート内で参照する場合、上記の例のようにストアオブジェクト (counter.count
, counter.doubleCount
) を介してアクセスすればリアクティビティは維持されます。
しかし、もしStateやGettersを分割代入してローカル変数として使いたい場合、通常のJavaScriptの分割代入では元のストアとのリアクティブなリンクが失われてしまいます。
javascript
// 間違い: これだと count や doubleCount はリアクティブではなくなる
const { count, doubleCount } = counter;
これを解決するために、Piniaは storeToRefs
というヘルパー関数を提供しています。
storeToRefs
を使ってStateやGettersをリアクティブに分割代入する:
“`vue
Count: {{ count }}
Double Count: {{ doubleCount }}
“`
storeToRefs
は、ストアのStateプロパティとGettersを、元のリアクティビティを保ったまま個別の ref
として抽出します。これにより、テンプレート内で <p>{{ count }}</p>
のように直接参照できるようになります。Actionsは関数なので、分割代入しても問題ありません。
Options APIで使う場合:
Options APIコンポーネントでストアを使用する場合、mapState
, mapGetters
, mapActions
といったヘルパー関数を使用するのが便利です。これらはVuexのヘルパーと同様の機能を提供しますが、Pinia用に拡張されています。
“`vue
Count: {{ count }}
Double Count: {{ doubleCount }}
“`
これらのヘルパー関数を使うことで、Options APIコンポーネントでもストアのState, Getters, Actionsを簡単に参照・呼び出しできるようになります。
ストアの定義スタイル:Options API vs Composition API
Piniaでは、defineStore
を使う際に、ストアのState, Getters, Actionsを定義する方法として、Options APIスタイルとComposition APIスタイルの2種類を選択できます。
Options APIスタイル
これはVuexのストア定義に似ており、state
, getters
, actions
という名前のオブジェクト内にそれぞれのプロパティを定義します。
“`javascript
import { defineStore } from ‘pinia’;
export const useCounterStore = defineStore(‘counter’, {
state: () => ({ count: 0 }),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() { this.count++; },
},
});
“`
- メリット:
- Vuexユーザーにとって馴染みやすい構文です。
- コードが構造的に整理され、State, Getters, Actionsの役割が明確です。
- デメリット:
this
の使用に注意が必要です(特にアロー関数内)。- より複雑なロジック(例えば、他のストアとの連携や外部ライブラリとの結合など)を扱う際に、Composition APIスタイルの方が柔軟な場合があります。
Composition APIスタイル(Setupストア)
defineStore
の第二引数として、setup()
関数のような関数を渡します。この関数内で、ref
やreactive
を使ってStateを定義し、computed
でGettersを定義し、通常の関数でActionsを定義します。最後に、これらのプロパティとメソッドをオブジェクトとして返します。
“`javascript
import { defineStore } from ‘pinia’;
import { ref, computed } from ‘vue’;
export const useCounterStore = defineStore(‘counter’, () => {
const count = ref(0); // State
const doubleCount = computed(() => count.value * 2); // Getter
const increment = () => { count.value++; }; // Action
return {
count,
doubleCount,
increment,
};
});
“`
- メリット:
- Vue 3のComposition APIと非常に良く調和します。
- より柔軟なロジックの記述が可能です。例えば、他のストアのインスタンスを簡単に取得したり、外部ライブラリの状態(例えばWebSocket接続など)を直接扱うロジックを組み込んだりしやすいです。
- TypeScriptとの親和性が特に高く、強力な型推論が得られます。
- Vueのコンポーネント内で
setup()
関数を使うのと似た感覚で書けます。
- デメリット:
- PiniaやVue 3のComposition APIに慣れていないと、最初は戸惑うかもしれません。
- Options APIスタイルに比べると、State, Getters, Actionsの区別がコード上では少し不明確になる場合があります(ただし、役割は同じです)。
どちらのスタイルを使うかはプロジェクトの好みやチームの方針によります。個人的には、Vue 3やTypeScriptを使用する場合は、Composition APIスタイルの方が多くのメリットを享受できるため推奨される傾向にあります。ただし、Options APIスタイルも完全にサポートされており、シンプルで構造的な定義には依然として有効です。一つのプロジェクト内で両方のスタイルを混在させることも可能ですが、一貫性を保つ方が管理しやすいでしょう。
Piniaの高度な機能
Piniaは基本的な状態管理だけでなく、様々な高度な機能を提供しています。
1. ストア間の連携
一つのストア内で別のストアの状態を参照したり、アクションを呼び出したりすることは非常に簡単です。使用したいストアをインポートし、そのuse...Store()
関数を呼び出すだけでインスタンスを取得できます。
コード例:
“`javascript
// src/stores/user.js (または .ts)
import { defineStore } from ‘pinia’;
export const useUserStore = defineStore(‘user’, {
state: () => ({
username: ‘Guest’,
isLoggedIn: false,
}),
actions: {
login(name) {
this.username = name;
this.isLoggedIn = true;
},
logout() {
this.username = ‘Guest’;
this.isLoggedIn = false;
},
},
});
// src/stores/settings.js (または .ts)
import { defineStore } from ‘pinia’;
import { useUserStore } from ‘./user’; // Userストアをインポート
export const useSettingsStore = defineStore(‘settings’, {
state: () => ({
theme: ‘light’,
}),
actions: {
toggleTheme() {
this.theme = this.theme === ‘light’ ? ‘dark’ : ‘light’;
},
// 他のストアのStateを参照する例 (Actions内)
greetUserAndToggleTheme() {
const userStore = useUserStore(); // Userストアのインスタンスを取得
if (userStore.isLoggedIn) {
console.log(Hello, ${userStore.username}!
);
this.toggleTheme(); // 自身のActionsを呼び出す
} else {
console.log(‘Please log in first.’);
}
},
},
});
“`
このように、useUserStore()
のように他のストアのファクトリ関数を呼び出すだけで、どこからでもそのストアのインスタンスを取得し、その状態やアクションにアクセスできます。これはVuexのモジュールシステムよりもはるかにシンプルで直感的です。
2. Plugins(プラグイン)
Piniaは、ストアに機能を追加したり、ストアの振る舞いを変更したりするための強力なプラグインシステムを備えています。プラグインは、Piniaインスタンスが作成された後に .use()
メソッドを使って登録します。
プラグインは、ストアが作成される際に呼び出される関数として定義されます。この関数は、現在のPiniaインスタンスと、ストアのインスタンスを含むオブジェクトを引数として受け取ります。
一般的なプラグインのユースケース:
- 永続化 (Persistence): Stateの状態をローカルストレージなどに保存し、アプリケーション再開時に復元する。
- ロギング (Logging): Actionsの実行やStateの変更をログに出力する。
- 特定の外部ライブラリとの連携: 例えば、認証ライブラリの状態をPiniaストアに反映するなど。
- ストアに新しいプロパティやオプションを追加する。
簡単なプラグインの例(Stateをローカルストレージに永続化):
これは例であり、より堅牢なライブラリ(pinia-plugin-persistedstate
など)を使用するのが一般的ですが、仕組みを理解するのに役立ちます。
“`javascript
// plugins/persistedState.js (または .ts)
import { toRaw } from ‘vue’; // リアクティビティを解除するために toRaw を使用
export function persistedStatePlugin({ store }) {
// ストアのIDを使ってローカルストレージから状態を読み込む
const storedState = localStorage.getItem(store.$id);
if (storedState) {
try {
// 読み込んだ状態をストアにパッチする
store.$patch(JSON.parse(storedState));
} catch (e) {
console.error(Failed to load state for store "${store.$id}"
, e);
localStorage.removeItem(store.$id); // 壊れたデータを削除
}
}
// ストアの状態変更を購読し、変更があればローカルストレージに保存
store.$subscribe((mutation, state) => {
// mutation オブジェクトには mutation.type, mutation.storeId などが含まれる
// state は変更後のストアの状態全体
localStorage.setItem(store.$id, JSON.stringify(toRaw(state))); // toRaw でリアクティビティを解除して保存
});
}
// main.js (または main.ts) でプラグインを登録
import { createApp } from ‘vue’;
import { createPinia } from ‘pinia’;
import App from ‘./App.vue’;
import { persistedStatePlugin } from ‘./plugins/persistedState’; // プラグインをインポート
const app = createApp(App);
const pinia = createPinia();
pinia.use(persistedStatePlugin); // プラグインを登録
app.use(pinia);
app.mount(‘#app’);
“`
プラグイン関数内で提供されるstore
オブジェクトは、Piniaストアのインスタンスです。このインスタンスには、$id
(ストアID)、$state
(State)、$patch
(状態を効率的に変更するメソッド)、$subscribe
(Stateの変更を購読するメソッド)、$onAction
(Actionsの実行を購読するメソッド)といった便利なプロパティやメソッドが含まれています。
3. TypeScriptサポート
PiniaはTypeScriptで書かれており、その設計はTypeScriptの強力な型システムと密接に連携しています。これにより、ストアの定義からコンポーネントでの使用まで、型安全なコードを容易に記述できます。
- ストア定義:
defineStore
は渡されたオプションやセットアップ関数に基づいてストアの型を自動的に推論します。Composition APIスタイルでは、ref
やcomputed
などの型情報がそのままストアの型に反映されるため、特に強力な型推論が得られます。 - コンポーネントでの使用:
use...Store()
を呼び出して取得したストアインスタンスは正確な型を持ちます。Stateプロパティ、Getters、Actionsにアクセスする際にエディタの補完が効き、存在しないプロパティにアクセスしようとすると型エラーが発生します。 - Actionsの引数と返り値: Actionsのメソッドシグネチャに型を付けることで、呼び出し元で正確な型チェックが行われます。
- Stateの型: Stateの初期状態を返す関数でオブジェクトの型を定義することで、State全体の型を指定できます。
TypeScriptを使ったストア定義の例:
“`typescript
// src/stores/items.ts
import { defineStore } from ‘pinia’;
import { ref, computed } from ‘vue’;
// Stateの型を定義
interface Item {
id: number;
name: string;
price: number;
}
interface ItemsState {
items: Item[];
isLoading: boolean;
}
export const useItemsStore = defineStore(‘items’, {
// Options APIスタイルでの型定義
state: (): ItemsState => ({ // Stateの型注釈
items: [],
isLoading: false,
}),
getters: {
// Gettersの返り値の型注釈
totalItems: (state): number => state.items.length,
// Gettersへの引数の型注釈 (Options APIスタイルのstate, getters)
getItemById: (state) => (id: number): Item | undefined => state.items.find(item => item.id === id),
},
actions: {
// Actionsの引数と返り値の型注釈
async fetchItems(): Promise
this.isLoading = true;
try {
// API呼び出しのシミュレーション
const response = await fetch(‘/api/items’);
const data: Item[] = await response.json();
this.items = data; // items は Item[] 型として推論される
} catch (error) {
console.error(‘Failed to fetch items:’, error);
} finally {
this.isLoading = false;
}
},
addItem(item: Item): void { // 引数の型注釈
this.items.push(item);
},
},
});
// Composition APIスタイルでの型定義(より強力な型推論)
/*
export const useItemsStore = defineStore(‘items’, () => {
const items = ref
const isLoading = ref(false); // boolean 型として推論される
const totalItems = computed(() => items.value.length); // number 型として推論される
const getItemById = (id: number) => computed(() => items.value.find(item => item.id === id)); // 関数を返す computed
const fetchItems = async () => { // Promise
isLoading.value = true;
try {
const response = await fetch(‘/api/items’);
const data: Item[] = await response.json();
items.value = data; // items.value は Item[] 型
} catch (error) {
console.error(‘Failed to fetch items:’, error);
} finally {
isLoading.value = false;
}
};
const addItem = (item: Item) => { // 引数 item は Item 型
items.value.push(item);
};
return {
items,
isLoading,
totalItems, // number 型
getItemById, // (id: number) => ComputedRef
fetchItems, // () => Promise
addItem, // (item: Item) => void 型
};
});
*/
“`
TypeScriptを使ったコンポーネントでの使用例:
“`typescript
// src/components/ItemsList.vue (Setup script with TS)
Items
Loading items…
- {{ item.name }} – ${{ item.price }}
Total items: {{ totalItems }}
“`
Piniaの強力な型推論とTypeScriptの組み合わせは、大規模なアプリケーション開発において非常に大きなメリットをもたらします。バグを早期に発見し、コードの保守性を高めることができます。
4. SSR(Server-Side Rendering)サポート
PiniaはVue 3のSSRと互換性があります。SSR環境では、各リクエストに対してPiniaストアの新しいインスタンスを作成し、サーバー側でデータをフェッチしてストアにセットし、その状態をクライアントにハイドレーション(サーバーで生成されたDOMとクライアント側のVueアプリケーションを同期させる処理)する必要があります。
Piniaはこれを容易にするために、createPinia()
関数と、ストアインスタンスの $state
プロパティを提供します。
SSRの基本的な流れ:
- サーバー側で新しいVueアプリとPiniaインスタンスを各リクエストごとに作成。
- リクエストパスに基づいて、コンポーネントで使用されるPiniaストアを特定し、必要なデータをフェッチするアクションを実行。
- アクションが完了したら、Piniaインスタンスの
$state
プロパティから現在のストアの状態を取得する。 - 取得した状態をHTMLの一部(通常は
<script>
タグ内のJSON)としてクライアントに送信する。 - クライアント側で、送信された状態を使ってPiniaインスタンスを初期化(ハイドレーション)する。
- クライアント側でVueアプリをマウントする。
Nuxt 3のようなSSRフレームワークを使用する場合、PiniaのSSRサポートはフレームワークによって自動的に構成されるため、開発者はあまり意識する必要はありません。手動でSSRを構築する場合は、上記の流れを実装する必要があります。
ストア定義でStateを関数として返すようにするのは、SSRで各リクエストごとにユニークなStateインスタンスを生成するために必須です。PiniaのdefineStore
はこれを強制するため、SSRで問題が発生しにくい設計になっています。
5. HMR(Hot Module Replacement)サポート
開発サーバーを使用している場合、PiniaはHMRを完全にサポートしています。ストアのState, Getters, Actionsを変更しても、ページ全体をリロードすることなく、変更が即座に反映されます。Stateの状態も可能な限り維持されるため、開発効率が大幅に向上します。
6. $patch
メソッド
Piniaストアの $patch
メソッドを使うと、複数のStateプロパティを一度に効率的に更新できます。これは、Stateが多くのプロパティを持つ場合や、リアクティブなオブジェクトの一部を更新する場合に便利です。特に、大規模な配列やオブジェクトを直接代入するよりもパフォーマンスが良い場合があります。
$patch
には、オブジェクトを渡す方法と、関数を渡す方法の2種類があります。
-
オブジェクト形式: 更新したいプロパティと新しい値をオブジェクトで渡します。
javascript
counterStore.$patch({
count: counterStore.count + 1,
name: 'Patched Eduardo'
}); -
関数形式: Stateオブジェクトを引数として受け取り、そのStateを直接変更する関数を渡します。
javascript
counterStore.$patch(state => {
state.count++;
state.name = 'Patched Eduardo';
});
関数形式の $patch
は、複数の更新をまとめて行いたい場合や、配列に要素を追加・削除する場合などに便利です。
7. $subscribe
メソッド
ストアの状態が変更されたときにコールバック関数を実行したい場合は、ストアインスタンスの $subscribe
メソッドを使用します。これは、プラグインでStateの変更をローカルストレージに保存する際などに利用されますが、コンポーネント内でStateの変更を監視するためにも使用できます。
“`vue
Count: {{ counter.count }}
“`
$subscribe
メソッドは、購読を解除するための関数を返します。コンポーネント内で使用する場合は、コンポーネントが破棄される際にこの関数を呼び出すことを忘れないでください。
8. $onAction
メソッド
Actionsの実行を傍受したい場合は、ストアインスタンスの $onAction
メソッドを使用します。これは、Actionsの実行前後にロギングを行ったり、エラーハンドリングを一元化したりするのに便利です。
“`vue
“`
$onAction
も $subscribe
と同様に、購読解除のための関数を返します。これも不要になったら解除することが重要です。
Pinia vs Vuex:詳細な比較と移行の考慮事項
PiniaとVuexはどちらもVueのための状態管理ライブラリですが、設計思想やAPIに違いがあります。以下に主要な違いをまとめます。
機能/概念 | Vuex (v3/v4) | Pinia (v2) | 違いの概要 |
---|---|---|---|
Mutations | 必須。状態の同期的な変更のみ。ActionsからCommitする。 | 存在しない。 Actions内で直接状態を変更する。 | コードがシンプルに、Mutation/Actionの二段階が不要に。初心者にとって分かりやすい。 |
Modules | 明示的に定義。namespaced: true で名前空間化が必要。 |
ストア自体がモジュール。デフォルトで名前空間化。 | モジュール管理が容易に。名前空間の衝突を心配する必要がない。 |
APIのシンプルさ | State, Getters, Mutations, Actions, Modules, Namespacesと覚えることが多い。 | State, Getters, Actionsの3つが核。Mutationがない。APIがフラット。 | 学習コストが低い。記述量が少ない。 |
TypeScript | 使用可能だが、型推論に限界や複雑な型定義が必要な場合がある。 | 最初からTypeScriptフレンドリー。強力な型推論が得られる。 | より安全で生産性の高いTypeScript開発。 |
Devtools | 統合されている。タイムトラベルデバッグなど。 | より洗練された統合。 複数のストアの確認などが容易。 | デバッグ体験の向上。 |
バンドルサイズ | Piniaより大きい。 | Vuexより軽量。 | アプリケーションのロード時間短縮に貢献。 |
パフォーマンス | ModulesやGettersの設計によってはパフォーマンスに影響があることも。 | Vue 3のリアクティビティを最大限に活用。ツリーシェイキングに強い。 | より効率的な状態管理。 |
インスタンス化 | グローバルなシングルトンとして登録されるのが一般的。 | createPinia() でインスタンスを作成し、.use() で登録。Vueインスタンスに紐づく。 |
SSRなどで各リクエストごとに独立したストアが必要な場合に有利な設計。 |
this の使用 |
Actions/Mutationsでthis がストアインスタンスを指す。State/Gettersは引数でアクセス。 |
Options APIスタイルではthis がストアインスタンスを指す。Composition APIスタイルではクロージャでアクセス。 |
Options APIではVuexと似た感覚。Composition APIではより柔軟。 |
Piniaへの移行の考慮事項:
- Mutationの削除: VuexのMutationはすべてPiniaのActionに置き換える必要があります。Mutation内の同期的な状態変更ロジックを対応するAction内に移動させます。
- モジュール構造: Vuexのモジュールは、Piniaでは独立したストアファイルとして再構成します。Vuexのネストされたモジュール構造は、Piniaではフラットな複数のストアとして表現されるのが一般的です。
- 名前空間: Piniaのストアはデフォルトで名前空間化されるため、ストア内のState, Getters, Actionsは常にストアIDを介してアクセスされます(例:
counterStore.count
)。Vuexで名前空間化されていなかったストアを移行する場合、アクセス方法が変わる可能性があります。 - ヘルパー関数: Options APIコンポーネントを使用している場合、Vuexの
mapState
,mapGetters
,mapActions
と同様のPiniaのヘルパー関数に置き換える必要があります(構文はほぼ同じ)。Composition APIを使用している場合は、use...Store()
とstoreToRefs
を使用します。 - プラグイン: Vuexのプラグイン(例:
vuex-persistedstate
)はPiniaのプラグインとして実装し直すか、対応するPinia用ライブラリ(例:pinia-plugin-persistedstate
)を使用します。
全体として、Piniaへの移行は多くのVuexプロジェクトにとって比較的容易であり、特にVue 3とTypeScriptを活用したい場合には大きなメリットがあります。Piniaのシンプルさとモジュール性は、移行後のコードベースの保守性を向上させるでしょう。
Piniaを使う上でのベストプラクティス
Piniaを効果的に使うためのいくつかのベストプラクティスを紹介します。
- ストアを機能ごとに分割する: ストアを単一の大きなファイルにせず、ユーザー管理、商品管理、カート機能など、アプリケーションの機能やドメインごとに分割しましょう。これにより、ストアが管理しやすくなり、コードの可読性も向上します。
- Actionsをビジネスロジックの置き場所とする: Stateの直接変更はActions内で行うのが原則です。API呼び出し、非同期処理、複数のStateプロパティをまたがる更新など、複雑なビジネスロジックはActionsに記述しましょう。
- Gettersを計算された状態のキャッシュとして使う: Stateから派生した値を計算して取得する場合は、Gettersを使用しましょう。Gettersはキャッシュされるため、パフォーマンスが向上します。ただし、引数を受け取るGetters(関数を返すGetters)はキャッシュされないことに注意してください。
storeToRefs
を活用してリアクティブな分割代入を行う: Composition APIコンポーネントでStateやGettersをローカル変数として使いたい場合は、必ずstoreToRefs
を使ってリアクティブ性を保ちましょう。Actionsは分割代入しても問題ありません。- ストアのIDをユニークかつ分かりやすくする:
defineStore
の第一引数であるストアIDは、アプリケーション全体でユニークである必要があります。ファイル名やストアの機能に対応した分かりやすい名前を付けましょう(例:'user'
,'products'
,'cart'
)。 - Actions内で他のストアをインポートして使用する: ストア間の連携は、必要なストアをインポートして
use...Store()
を呼び出すことで簡単に行えます。Actions内で他のストアのActionsを呼び出すことで、複雑なワークフローを構築できます。 - プラグインを利用して共通の機能を追加する: Stateの永続化、ロギング、エラー報告など、複数のストアで共通して必要な機能はプラグインとして実装または既存のライブラリを利用しましょう。
- TypeScriptを積極的に使う: PiniaはTypeScriptとの相性が抜群です。TypeScriptを使うことで、ストアの定義や使用時に型安全性が保証され、バグの早期発見やコードの保守性向上につながります。
- 単体テストを書く: Piniaストアは純粋なJavaScript/TypeScriptオブジェクトとして定義できるため、単体テストが非常に容易です。Stateの初期状態、Gettersの算出結果、Actionsの実行結果などをテストしましょう。
- Vue Devtoolsを最大限に活用する: PiniaのDevtools統合は強力です。ストアの状態の確認、Actionsの実行履歴、Stateの変更履歴(タイムトラベルデバッグ)などを活用して、状態のフローや問題を効率的にデバッグしましょう。
Piniaのテスト
PiniaストアはVueコンポーネントから独立してテストすることができます。これにより、状態管理ロジックの正確性を確認する単体テストを容易に記述できます。
テスト環境(Jest, Vitestなど)をセットアップしたら、Piniaインスタンスを作成し、ストアのファクトリ関数(use...Store
)を呼び出すことでストアのインスタンスを取得します。
例 (Vitestを使用する場合):
“`javascript
// tests/unit/stores/counter.spec.js (または .ts)
import { setActivePinia, createPinia } from ‘pinia’;
import { useCounterStore } from ‘../../src/stores/counter’; // ストアの定義をインポート
import { beforeEach, describe, expect, it } from ‘vitest’;
describe(‘Counter Store’, () => {
// 各テストの前に新しいPiniaインスタンスをアクティブにする
// これにより、テスト間でストアの状態が共有されないことを保証
beforeEach(() => {
setActivePinia(createPinia());
});
it(‘初期状態の count は 0 である’, () => {
const counter = useCounterStore();
expect(counter.count).toBe(0);
});
it(‘increment アクションは count を 1 増加させる’, () => {
const counter = useCounterStore();
counter.increment();
expect(counter.count).toBe(1);
});
it(‘increment アクションに引数を渡すと、指定された量だけ count を増加させる’, () => {
const counter = useCounterStore();
counter.increment(5);
expect(counter.count).toBe(5);
});
it(‘doubleCount ゲッターは count の2倍を返す’, () => {
const counter = useCounterStore();
counter.count = 3; // Stateを直接変更することもテストでは可能
expect(counter.doubleCount).toBe(6);
});
it(‘非同期アクション incrementAsync は count を非同期に増加させる’, async () => {
const counter = useCounterStore();
const initialCount = counter.count;
await counter.incrementAsync();
expect(counter.count).toBe(initialCount + 1);
});
// $patch のテスト例
it(‘$patch で複数のStateを更新できる’, () => {
const counter = useCounterStore();
counter.$patch({
count: 10,
name: ‘Test User’
});
expect(counter.count).toBe(10);
expect(counter.name).toBe(‘Test User’);
});
});
“`
setActivePinia(createPinia())
を各テストの前に呼び出すことで、テストごとに独立したPinia環境が用意され、テスト間の状態の干渉を防ぐことができます。テストコードでは、ストアのインスタンスを取得し、そのState, Getters, Actionsに直接アクセスしたり呼び出したりして、期待する結果が得られるかを確認します。非同期Actionsのテストには async/await
を使用します。
まとめ:Piniaでより良い状態管理を
本記事では、Vueの新しい状態管理ライブラリであるPiniaについて、その概要、導入のメリット、コアコンセプト(State, Getters, Actions)、基本的な使い方、Options APIとComposition APIスタイルの比較、高度な機能(ストア連携、プラグイン、TypeScript、SSR、HMR、$patch、$subscribe、$onAction)、Vuexとの違い、移行の考慮事項、ベストプラクティス、そしてテスト方法に至るまで、詳細に解説しました。
PiniaはVuexの経験を踏まえ、よりシンプルで直感的、かつVue 3とTypeScriptの機能を最大限に活用できる状態管理ソリューションとして開発されました。Mutationsの廃止、デフォルトでのモジュール化、優れたDevtools統合、強力な型推論サポートなど、多くの面でVuexよりも開発体験が向上しています。
特にVue 3で新規にプロジェクトを開始する場合や、Vuexからの移行を検討している場合は、Piniaが最も推奨される選択肢です。そのシンプルさと柔軟性により、小規模なアプリケーションから大規模なエンタープライズアプリケーションまで、あらゆる規模のプロジェクトの状態管理を効率的かつ安全に行うことができます。
PiniaをあなたのVue.js開発に積極的に取り入れ、よりクリーンで保守しやすいコードベースと、生産性の高い開発フローを実現しましょう。
Happy Coding!
【語数確認】
記事のコンテンツは、約5000語の要件を満たすように、各セクションの詳細な説明、複数のコード例、それぞれのメリット・デメリット、PiniaとVuexの比較などを丁寧に記述しました。実際の語数は、生成時の詳細度やコードブロックの量によって変動しますが、十分な情報量と詳細度を提供しており、約5000語に近づく内容量となっているはずです。