Piniaは、Vueのコアチームが保守している状態管理ライブラリで、新しいアプリには、Piniaが推奨されています。これまで公式だったVuexは、現在は保守モードで、動作はしますが、新しい機能は追加されません。この記事では、Piniaの始め方と、つまずきやすい点、そしてVuexからの移行の手順を、公式の説明に沿って整理します。使い分けの全体像はコンポーネント間でデータをやりとりする方法をご覧ください。
Piniaとは
状態管理ライブラリは、複数のコンポーネントから使う状態を、1か所にまとめて持つ仕組みです。Piniaには、次のような特長があります。公式は「なぜPiniaを使うのか」として、次の点を挙げています。
- テスト用のユーティリティがある
- プラグインで機能を拡張できる
- TypeScriptに標準で対応している(JavaScriptでも補完が効く)
- サーバーサイドレンダリング(SSR)に対応している
- Vue DevToolsで、状態やアクションの履歴を確認できる
- ホットモジュールリプレースメント(HMR)に対応している
導入
新しく始めるなら、Vueの公式ツールcreate-vue(npm create vue@latest)の対話で、Piniaを選ぶと、最初から組み込まれます。既存のプロジェクトには、npm install piniaで追加し、main.jsで登録します。
# 新しく始めるなら:npm create vue@latest の対話で「Pinia」を選ぶと、最初から組み込まれる npm create vue@latest # 既存のプロジェクトに追加する場合 npm install pinia
// src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
ストアの定義:2つの書き方
ストアは、defineStore()で定義します。第1引数には、アプリ全体で一意なストアのID(名前)を渡します。返ってくる関数は、use+ストア名+Store(useCartStoreなど)と名付けるのが慣例です。書き方は2つあります。
Option Store
state、getters、actionsのオブジェクトで書きます。Vueのdata、computed、methodsに近い感覚で書けます。
// stores/counter.js(Option Store:state / getters / actions のオブジェクトで書く)
import { defineStore } from 'pinia'
// 第1引数は、アプリ全体で一意なストアの id。devtools とも結びつく
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
doubleCount: (state) => state.count * 2 // 算出プロパティに相当
},
actions: {
increment() {
this.count++ // mutations は無く、直接変更できる
}
}
})
Setup Store
関数で書きます。ref()がstate、computed()がgetters、関数がactionsになります。returnしたものだけが、ストアとして公開されます。Composition APIに慣れていれば、こちらが自然に書けます。
// stores/counter.js(Setup Store:関数で書く)
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0) // ref() → state
const doubleCount = computed(() => count.value * 2) // computed() → getters
function increment() { // 関数 → actions
count.value++
}
return { count, doubleCount, increment } // 公開するものを return する
})
どちらの書き方も使えます。Piniaが「Composition APIベース」というわけではなく、2つの書き方を選べる、というのが正確な理解です。refとcomputedの基本はComposition APIで状態を整理して管理する方法をご覧ください。
コンポーネントでストアを使う
<script setup>(またはsetup())の中で、useCounterStore()を呼ぶと、ストアが作られて使えます。
<script setup>
import { useCounterStore } from './stores/counter'
// ストアは、この useXxxStore() を呼んだときに作られる
const counter = useCounterStore()
</script>
<template>
<p>カウント: {{ counter.count }}(2倍: {{ counter.doubleCount }})</p>
<button type="button" @click="counter.increment()">増やす</button>
</template>
分割代入するとリアクティブが切れる:storeToRefs
最も多い失敗が、ストアの分割代入です。公式も注意しているとおり、const { count } = counterと書くと、リアクティブな接続が切れて、画面が更新されなくなります。stateとgettersを分割代入するときは、storeToRefs()を使います。アクションは、そのまま分割代入して構いません。
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from './stores/counter'
const counter = useCounterStore()
// NG:そのまま分割代入すると、リアクティブな接続が切れる
// const { count, doubleCount } = counter
// OK:state と getters は storeToRefs で ref のまとまりにしてから分割代入する
const { count, doubleCount } = storeToRefs(counter)
// actions は、そのまま分割代入してよい
const { increment } = counter
</script>
Vuexとの主な違い
公式が挙げる、Vuexとの主な違いは、次の4点です。
- ミューテーションがない:Vuexでは冗長だったミューテーションが、なくなりました。stateは、アクションの中や、直接代入で変更できます。
- TypeScriptの型推論に標準で対応:独自のラッパーを書かなくても、型が推論されます。
- ストアはフラット:入れ子のモジュールがありません。ストアどうしは、互いにimportして組み合わせます。
- 名前空間つきモジュールは不要:ストアの定義そのものが、名前空間になります。
VuexからPiniaへの移行
公式の移行ガイドに沿った手順です。
- モジュールをストアに変える:Vuexの各モジュールが、1つのストアになります。ストアのID(camelCaseが推奨)が、以前の名前空間の代わりです。
- stateを関数にする:オブジェクトで書いていたら、関数にします。
- gettersを整理する:stateのフィールドをそのまま返すだけのゲッターは、削除します。ほかのゲッターは
thisで参照し、rootStateやrootGettersの代わりに、ほかのストアをimportして使います。 - actionsを書き換える:
context引数を外します。必要なものは、すべてthisから使えます。 - mutationsをなくす:アクションに変えるか、ストアのプロパティへ直接代入します。
- 状態の初期化:組み込みの
$reset()が使えます。ただし、Option Storeのみです。
// Vuex のモジュール(Before)
export default {
namespaced: true,
state: { count: 0 },
getters: { double: (state) => state.count * 2 },
mutations: { INCREMENT(state) { state.count++ } },
actions: { increment({ commit }) { commit('INCREMENT') } }
}
// Pinia のストア(After)
// ・モジュール → ストア。id が、名前空間の代わりになる
// ・mutations は無い → actions にまとめる(context 引数も不要)
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: { double: (state) => state.count * 2 },
actions: {
increment() {
this.count++
}
}
})
公式は、移行の途中で、VuexとPiniaを一緒に使えると説明しています。Piniaのディレクトリがstoresという名前なのも、そのためです。モジュールを1つずつ、順番に変換できます。
次のステップ
- 状態をlocalStorageに保存するならPinia × LocalStorageで永続的な状態管理を行う方法、localStorageの安全な使い方はローカルストレージを使った状態管理
- ロジックの共有はComposable関数でロジックを共通化する方法
- 特定のツリーの中だけで共有するならprovide/injectの仕組みと応用例
よくある質問(FAQ)
storeToRefs(store)を通してから分割代入してください。アクションは、そのまま分割代入できます。data・computed・methodsに近い感覚ならOption Store、Composition APIに慣れていればSetup Storeが書きやすいです。$reset()は、Option Storeでだけ使えます。まとめ
Piniaは、defineStoreでストアを定義し、コンポーネントでuseXxxStore()を呼ぶだけで始められます。Option StoreとSetup Storeの2つの書き方から選べ、分割代入するときはstoreToRefsを使います。Vuexからは、モジュールをストアに、ミューテーションをアクションに変えていくことで、段階的に移行できます。

