Pinia(読み方:ピーニャ)は、Vue.jsの公式の状態管理ライブラリです。複数のコンポーネントから読み書きするデータを「ストア」にまとめ、Vuexの後継としてVue公式ドキュメントで推奨されています。最新版は2026年8月12日公開の4.0.3で、4系からは @vue/devtools-api を一緒にインストールしないと読み込みエラーになります。本記事では、Vuexとの違い、4系の変更点、ストアの書き方と使い方の注意点を、2026年9月28日時点のnpmレジストリと公式ドキュメント(v4.0.3のソース)、ローカル環境(Pinia 4.0.3・Vue 3.5.43・Node.js 26.5.0・Vitest 5.0.2)で動かした結果をもとに解説します。
まとめ:Piniaの要点
- 正体:Vueのコアチームが保守する公式の状態管理ライブラリ。Vue公式ドキュメントはVuexを「maintenance mode」とし、新規開発にはPiniaを推奨している。
- 読み方:「ピーニャ」。スペイン語でパイナップルを意味する piña に由来する。
- Vuexとの違い:mutationsが無く、stateをactionsや直接代入で書き換える。モジュールの入れ子や名前空間の代わりに、小さなストアを複数並べる。
- 最新版:4.0.3。4系はESMのみの配布になり、
npm install pinia @vue/devtools-apiの2パッケージ導入が公式手順。Vue 2で使えるのは2系まで。 - 使い方の注意:ストアを分割代入するとリアクティブ性が切れるので
storeToRefs()を使う。Setup Storeには$reset()が無いので自分で定義する。
Piniaの位置づけ:Vue公式の状態管理ライブラリ
Piniaは、Vuexの次期版(Vuex 5)の構想を試す実験として始まりました。公式ドキュメントは「Piniaが既にVuex 5で実現したかったことの大半を実装していると分かり、代わりにPiniaを新しい推奨にした」と経緯を説明しています。現在はVueエコシステムの他のコアライブラリと同じRFCの手続きで仕様が決まり、APIは安定段階に入っています。
読み方「ピーニャ」と名前の由来
公式ドキュメントには「Pinia (pronounced /piːnjʌ/, like “peenya” in English)」とあり、日本語では「ピーニャ」が近い発音です。piña(スペイン語でパイナップル)に最も近く、npmで使えるパッケージ名として選ばれました。パイナップルは小さな花が集まって1つの実になることから、「個別に生まれたストアが最後にはつながる」というストアの設計を表しています。ロゴがパイナップルなのもこのためです。
ストアに置くデータと置かないデータ
公式が挙げるストアの用途は、ナビゲーションバーに出すユーザー情報のようにアプリの複数箇所で使うデータと、複数ページにまたがる入力フォームのようにページ遷移後も保持したいデータです。反対に、ある画面の要素の表示・非表示のようにコンポーネント内で完結するデータは、ストアに入れず ref で持つよう勧めています。処理を共有したいだけなら、ストアではなくコンポーザブル(use で始まる関数)で足ります。
PiniaとVuexの違い:mutations廃止と保守モード
Vuexは3系がVue 2用、4系がVue 3用です。npmの最終リリースは2022年10月14日の4.1.0で、それ以降の公開はありません。VuexのREADMEは「Vuex 3 and 4 will still be maintained. However, it’s unlikely to add new functionalities to it.」と書き、Vue公式ドキュメントの状態管理のページも「Vuex is now in maintenance mode」「It is recommended to use Pinia for new applications.」としています。
| 項目 | Vuex 4 | Pinia 4 |
|---|---|---|
| 状態の変更 | mutationsをcommit | actionsまたは直接代入 |
| ストアの構成 | 1ストア+modules | 複数ストアを並列 |
| 名前空間 | namespaced: true | ストアIDがそのまま役割 |
| 呼び出し | 文字列(’cart/add’) | 関数を直接import |
| TypeScript | 型の補助定義が必要 | 定義から型推論 |
| 最新リリース | 4.1.0(2022-10-14) | 4.0.3(2026-08-12) |
mutationsはもともとVue Devtoolsで変更を追跡するための仕組みでしたが、Piniaでは直接代入もDevtoolsに記録されるため不要になりました。文字列でactionを呼ぶ代わりに関数をimportするので、エディタの補完と型チェックが効きます。ストア同士の相互利用は可能ですが、Setup Storeの初期化中に互いのstateを直接読み合う構成や、getters・actionsが無限に呼び合う構成は避ける必要があります。
Piniaのバージョン:4系の変更点と2系・3系との違い
Piniaのメジャーバージョンは、新機能よりも対応環境の整理で上がってきました。3.0も4.0も、リリースノートは「新機能は無い」「技術的な破壊的変更のみ」と説明しています。各版の変更内容はPiniaのCHANGELOGで確認できます。
| 版 | 公開日 | 主な変更 |
|---|---|---|
| 2.0.0 | 2021-10-27 | Vue 2とVue 3の両対応 |
| 3.0.0 | 2025-02-11 | Vue 2対応と非推奨APIを削除 |
| 4.0.0 | 2026-07-14 | ESMのみ、devtools-api v8が必須 |
| 4.0.3 | 2026-08-12 | SSRのSet・Map復元などを修正 |
3.0では、Vue 2対応に加えて PiniaStorePlugin 型と defineStore({ id: 'id' }) の書き方が削除されました。4.0では配布がESMのみになり、エラーと開発時の警告がNosticsという診断ライブラリで作り直されています。4.0.3のpackage.jsonに記載されたpeer dependency(TypeScriptは任意依存で、使用する場合の最低バージョン)は、vue ^3.5.11・typescript >=5.6.0・@vue/devtools-api ^8.1.5 です。
Pinia 4へ上げるときの確認点
最も影響が大きいのは @vue/devtools-api です。公式のインストール手順は「Since v4, you must also install @vue/devtools-api」と明記しています。npm 7以降は通常peer dependencyを自動で入れますが、--legacy-peer-deps 付きでPinia 4.0.3とVue 3.5.43だけを入れて import { createPinia } from 'pinia' を実行すると、Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@vue/devtools-api' imported from .../pinia/dist/pinia.js で起動に失敗しました。peerを自動で入れない設定のプロジェクトでは、package.jsonに明示しておくのが安全です。
ESMのみになった点は、Node.js 26.5.0では require('pinia') でも読み込めました(Node.jsのrequire(esm)機能による)。CommonJS前提の古いテスト設定やビルドツールを使っているなら、上げる前にテストを一度通して確かめてください。なお、Vue公式ドキュメントの状態管理のページには「works with both Vue 2 and Vue 3」と書かれていますが、これはPinia 2系までの話です。Vue 2.7のプロジェクトで使えるのはPinia 2.3系までです。
Piniaのインストールと初期設定
ViteやVue公式の create-vue で作ったVue 3プロジェクトなら、次のコマンドと初期設定で導入できます。create-vue の質問でPiniaを選ぶと、ここまでの設定が最初から入ったプロジェクトが生成されます。
npm install pinia @vue/devtools-api
次に、ルートの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')
yarn・pnpm・bun・denoでも、パッケージ名は同じ2つです。Nuxtを使う場合は、後述するモジュールで導入します。
ストアの定義:Option StoreとSetup Storeの書き分け
ストアは defineStore() で定義し、第1引数のID(アプリ内で一意の文字列)がDevtoolsの表示名やSSRのシリアライズのキーになります。書き方は2種類あります。
Option Store:Options APIに近い書き方
state・getters・actions の3つを持つオブジェクトで定義します。公式はこの3つを、コンポーネントの data・computed・methods に相当すると説明しています。
// src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, items: [] }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
Setup Store:Composition APIと同じ書き方
関数の中で ref(state)・computed(getters)・関数(actions)を作り、返します。ストアの中で watch を使ったり、他のコンポーザブルを呼んだりできるのが利点です。
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const total = computed(() => items.value.reduce((sum, i) => sum + i.price, 0))
function add(item) {
items.value.push(item)
}
return { items, total, add }
})
公式は「stateのプロパティはすべて返す必要がある」と強調しています。返さない、あるいは readonly で包んだstateは、SSR・Devtools・プラグインが扱えなくなります。
Option StoreとSetup Storeの選択基準
公式の立場は「慣れている方を選べばよい」で、Option Storeは扱いやすく、Setup Storeは柔軟です。判断の目安は、コンポーネントをどちらのAPIで書いているかです。<script setup> で書いているチームならSetup Storeに揃えると、ストアとコンポーネントで書き方を切り替えずに済みます。一方、次章の $reset() のように、Option Storeにしか無い機能もあります。Composition APIそのものの書き方はComposition APIとは?書き方とOptions APIとの違い・移行判断を解説で解説しています。
コンポーネントでの使い方:分割代入とstoreToRefs
ストアは定義した関数(useCounterStore())を呼ぶと取得できます。同じPiniaインスタンス内では、同じIDのストアを何度呼んでも同じストアインスタンスが返り、実測でも useCounterStore() === store は true でした。
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
const { count, double } = storeToRefs(store) // stateとgettersはref化して取り出す
const { increment } = store // actionsはそのまま取り出せる
</script>
<template>
<button @click="increment">{{ count }}(2倍: {{ double }})</button>
</template>
ストアは reactive で包まれたオブジェクトなので、const { count } = store と分割代入すると、その時点の値がコピーされるだけです。実測では、分割代入した count は1のまま、ストア側は2に増えていました。storeToRefs() で取り出した値はrefになり、ストアの変更に追従します。actionsはストアに束縛されているので、そのまま分割代入して呼んでも動きます。
コンポーネント外(Vue Routerのナビゲーションガードなど)でストアを使うときは、app.use(pinia) の後で呼ぶ必要があります。SSRでは、どのリクエストのPiniaかを区別するため useStore(pinia) のようにインスタンスを渡します。
stateの更新・リセット・監視:$patch・$reset・$subscribe
stateは store.count++ のように直接書き換えられます。複数の値をまとめて変えるときは $patch を使います。配列への追加のように、オブジェクトでは表しにくい変更は関数形式で渡します。
store.$patch({ count: 10 })
store.$patch((state) => {
state.items.push('a')
})
Setup Storeに$resetが無い問題
Option Storeでは store.$reset() でstateを初期値に戻せます。Setup Storeで同じ呼び出しをすると、Pinia 4.0.3では「Store “cart” is built using the setup syntax and does not implement $reset().」というエラーになりました。公式ドキュメントは、Setup Storeでは自分で $reset を定義する方法を示しています。
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function $reset() {
count.value = 0
}
return { count, $reset }
})
$subscribeで変更を検知するときの注意
$subscribe はstateの変更ごとにコールバックを呼び、変更の種類を 'direct'(直接代入)・'patch object'・'patch function' で渡します。既定の設定では、同じtick内の直接代入はまとめて1回として通知されます。store.count++ を2回続けた後に nextTick() を待つと、通知は1回でした。第2引数に { flush: 'sync' } を渡すと直接代入のたびに通知され、同じ操作で通知は2回になりました。関数形式の $patch() は中で何度値を変えても1回の通知なので、通知回数を変更回数として数えることはできません。公式ドキュメントの例は、変更のたびにstate全体をlocalStorageへ保存する使い方です。永続化が目的なら、pinia-plugin-persistedstate(4.7.1、peer dependencyは pinia >=3.0.0)でストアに persist: true を付ける方法もあります。
NuxtでPiniaを使う設定
Nuxtでは公式モジュール @pinia/nuxt(最新1.0.2)を使います。公式ドキュメントはNuxt 3と4への対応を明記しており、1.0.2のリリースノートではNuxt 5への対応も宣言されました。導入は次のコマンドで、nuxt.config の modules に '@pinia/nuxt' が追加されます。
npx nuxi@latest module add pinia
モジュールは defineStore()・storeToRefs()・usePinia()・acceptHMRUpdate() を自動インポートし、stores ディレクトリ(Nuxt 4では app/stores)のストアも自動で読み込みます。入れ子のディレクトリは対象外なので、使う場合は pinia.storesDirs で指定します。SSRでは、stateのシリアライズとXSS対策をNuxtが受け持ちます。ページでデータを取得するactionは、await callOnce('user', () => store.fetchUser()) のように callOnce() で包むと、サーバーで取得済みのデータをクライアントで再取得しません。Nuxt 4のディレクトリ構成はNuxt 4とは?インストール・最新バージョン確認・Nuxt 3との違いとPinia連携まで使い方を解説で詳しく扱っています。
VuexからPiniaへの移行手順
VuexとPiniaは同じプロジェクトに同時にインストールできるので、画面や機能の単位で段階的に移せます。公式の移行ガイドが示す対応関係は次の4点です。
- モジュール:Vuexのモジュール1つをPiniaのストア1つにする。入れ子のモジュールも、それぞれ独立したストアにする。
- 名前空間:ストアIDに旧名前空間に対応する名前を付ける(公式の例では
nested/module2を'nestedModule2'にしている)。フォルダ名もstoreではなく複数形のstoresにする。 - mutations:actionsに統合する。単純な代入だけのmutationは削除し、呼び出し側で直接代入するか
$patchを使う。 - rootState・rootGetters:他のストアをimportして呼ぶ形に書き換える。
コンポーネント側で mapState・mapActions を使っていた箇所は、Piniaにも同名のヘルパー(mapState()・mapActions()・mapStores())があるので、Options APIのままでも書き換えられます。移行の優先度を判断する材料として、Vuexには2022年10月以降リリースが無く、新機能が追加されないことを押さえておいてください。
Piniaのテスト:createTestingPiniaの既定動作
コンポーネントのテストでは、@pinia/testing(最新2.0.1)の createTestingPinia() を使います。見落としやすいのは、既定でactionsがすべてスタブ(呼び出しを記録するだけの空関数)に置き換わる点です。Vitest 5.0.2で確かめると、increment() を呼んでも呼び出し回数だけが1になり、stateは0のままでした。
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { vi } from 'vitest'
setActivePinia(createTestingPinia({
createSpy: vi.fn, // globals: true でなければ必須
stubActions: false, // actionsを実際に実行する
initialState: { counter: { count: 20 } }, // ストアIDごとに初期stateを指定
}))
もう1つの落とし穴は、Setup Storeの中でaction同士を呼ぶ場合です。incTwice() の中から inc() を2回呼ぶストアで stubActions: false にして incTwice() を実行すると、stateは2になりましたが、inc のスパイの呼び出し回数は0でした。ストア内部の呼び出しは閉じ込められた関数を直接呼ぶため、スパイを経由しません。公式ドキュメントにも同じ注意があります。内部呼び出しを検証したいなら、呼び出し回数ではなくstateの結果で確かめます。Vitestでのコンポーネントテストの組み方はVitestの使い方|React Testing Libraryでコンポーネントテストを書く実践ガイド【2026年版】も参考になります。
Piniaを使わない方がよい場面
Piniaは導入が軽いぶん、何でもストアに入れてしまいがちです。次の場合は使わない判断をしてください。
- 親子2〜3階層で完結するデータ:propsとemit、または
provide/injectで足りる。ストアに入れると、どの画面が値を変えたかを追いにくくなる。 - 処理の共有だけが目的:状態を持たない、あるいは呼び出し元ごとに別の状態を持つならコンポーザブルにする。同じPiniaインスタンス内ではストアIDごとに1つのインスタンスを共有する。SSRではリクエストごとにPiniaインスタンスを分ける。
- サーバーから取ってきたデータのキャッシュ:Pinia本体のstateとactionsだけで作ると、再取得・キャッシュの破棄・読み込み中の状態を手で管理することになる。避けたいのはこのキャッシュ管理の自作で、Piniaの導入そのものではない。Piniaの作者Eduardo San Martin Morote氏が開発する
@pinia/colada(1.4.6、npmの説明は「The smart data fetching layer for Vue.js」)は、Piniaを土台にこの部分を受け持つデータ取得用のライブラリで、自作より短く書ける。 - Vue 2.6以前のプロジェクト:Vue 2.6で使う場合は、Vue 2.6対応のPinia 2.2系以前と
@vue/composition-apiが必要になる。Pinia 2.3以降はVue 2.7以上が必要で、3系以降はVue 2に対応しない。Vue 3への移行と合わせて検討する。
React向けの状態管理を比べる場合はZustandとは?Reactの状態管理を最小コードで実現する使い方を徹底解説【v5対応】を参照してください。PiniaはVueのリアクティブシステムの上に作られているため、Reactでは使えません。
よくある質問
Piniaの読み方は?
「ピーニャ」です。公式ドキュメントが発音を /piːnjʌ/(英語の “peenya” に近い)と示しており、スペイン語でパイナップルを意味する piña が由来です。
VuexとPiniaはどちらを使うべきですか?
新規開発ならPiniaです。Vue公式ドキュメントはVuexを保守モードとし、新しいアプリにはPiniaを推奨しています。既存のVuexプロジェクトは動き続けますが、2022年10月の4.1.0以降リリースが無いため、段階的な移行を計画してください。
Piniaの最新バージョンは?
2026年9月28日時点で4.0.3(2026年8月12日公開)です。4系は2026年7月14日に出ました。最新はnpmで npm view pinia version を実行すると確認できます。
PiniaはReactでも使えますか?
使えません。PiniaはVueのリアクティブシステム(ref・reactive)に依存しており、peer dependencyにもVueが入っています。ReactならZustandやRedux Toolkitが相当します。
Piniaのstateをリロード後も残せますか?
Pinia本体には永続化の機能はありません。pinia-plugin-persistedstate を pinia.use() で登録し、ストアに persist: true を付けると、既定でlocalStorageに保存・復元されます。