はじめに
Vue.jsでアプリケーションが大きくなってくると、コンポーネント間でどうやって状態を共有するかが悩みの種になります。propsのバケツリレーやイベントの受け渡しが複雑に絡み合い、「このデータ、どこで更新されているんだっけ?」と追いかけ回した経験がある方も多いのではないでしょうか。
かつてVueの状態管理といえばVuexが定番でしたが、ミューテーションの記述量が多く、TypeScriptとの相性にも課題がありました。そこで登場したのがPiniaです。今ではVueコアチームによって公式に推奨される状態管理ライブラリとなり、多くの新規プロジェクトでVuexに代わる選択肢として採用されています。
Piniaとは
Piniaは、Vue.js向けの軽量な状態管理ライブラリです。Composition APIをベースに設計されており、型安全性・シンプルさ・DevToolsとの連携を兼ね備えています。開発者はEvan You氏率いるVueコアチームのメンバーでもあるEduardo San Martin Morote氏で、現在はVuexの実質的な後継としてVue公式ドキュメントでも案内されています。
主な特徴
- ミューテーション不要 - Vuexで必須だった
mutationsが廃止され、actionsから直接stateを更新できます。同期・非同期処理を区別なく書けるため、コードがシンプルになります。 - TypeScriptとの高い親和性 - 型推論が効くように設計されており、追加の型定義をほとんど書かずにstate・getters・actionsの型補完が得られます。
- モジュール構造がフラット - Vuexのようなネストしたモジュール構造を持たず、ストアごとに独立したファイルとして定義します。必要なストアだけをインポートするため、コード分割とも相性が良好です。
- DevTools対応 - Vue DevTools上でstateの変化やタイムトラベルデバッグが可能です。
- 軽量 - 依存が少なく、バンドルサイズへの影響を抑えた設計になっています。
- SSR対応 - Nuxtなどのサーバーサイドレンダリング環境でも問題なく利用できます。
インストール
Vue 3プロジェクトへのインストールは非常にシンプルです。
# npm
npm install pinia
# yarn
yarn add pinia
# pnpm
pnpm add pinia
Nuxtを利用している場合は、公式モジュールを使うとより簡単に導入できます。
npm install @pinia/nuxt
Vueアプリへの登録はmain.ts(またはmain.js)で行います。
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
基本的な使い方
PiniaではdefineStoreという関数を使ってストアを定義します。ここでは、シンプルなカウンターストアを例に見ていきましょう。
// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
async incrementAsync() {
await new Promise((resolve) => setTimeout(resolve, 500))
this.count++
},
},
})
コンポーネント側では、useCounterStoreを呼び出すだけでストアにアクセスできます。
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
<template>
<div>
<p>カウント: {{ counter.count }}</p>
<p>2倍の値: {{ counter.doubleCount }}</p>
<button @click="counter.increment">+1</button>
<button @click="counter.incrementAsync">+1(非同期)</button>
</div>
</template>
Composition APIスタイルで書きたい場合は、setup関数の形式でもストアを定義できます。refがstate、computedがgetters、通常の関数がactionsに対応します。
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
const name = ref('')
const isLoggedIn = computed(() => name.value !== '')
function login(userName: string) {
name.value = userName
}
function logout() {
name.value = ''
}
return { name, isLoggedIn, login, logout }
})
実践的なユースケース
実際のアプリケーションでは、API通信の結果をストアで管理するケースがよくあります。ここでは、記事一覧を取得するストアを例にします。
// stores/articles.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
interface Article {
id: number
title: string
}
export const useArticleStore = defineStore('articles', () => {
const articles = ref<Article[]>([])
const isLoading = ref(false)
const errorMessage = ref('')
async function fetchArticles() {
isLoading.value = true
errorMessage.value = ''
try {
const response = await fetch('/api/articles', {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
if (!response.ok) {
throw new Error('記事の取得に失敗しました')
}
articles.value = await response.json()
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '不明なエラーが発生しました'
} finally {
isLoading.value = false
}
}
return { articles, isLoading, errorMessage, fetchArticles }
})
複数のコンポーネントから同じストアを参照すれば、一度取得したデータをアプリ全体で使い回せます。たとえば一覧画面と詳細画面の両方で同じuseArticleStoreを呼び出しても、Piniaはシングルトンとしてインスタンスを管理してくれるため、状態が自動的に共有されます。
またテストの際には@pinia/testingパッケージを使うことで、ストアをモック化しやすくなっています。
import { setActivePinia, createPinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { useCounterStore } from '@/stores/counter'
describe('counter store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('incrementでcountが増える', () => {
const counter = useCounterStore()
counter.increment()
expect(counter.count).toBe(1)
})
})
ストア単位でテストが完結するため、Vuexのようにモジュール全体をモックする手間もかかりません。
まとめ
Piniaは、ミューテーションの廃止によるシンプルな記述、TypeScriptとの高い親和性、DevToolsによるデバッグのしやすさを兼ね備えた、Vue.js向けの状態管理ライブラリです。Vueコアチームによって公式に推奨される立場となったこともあり、これからVue 3でアプリケーションを構築するなら第一候補となる存在といえます。
まだVuexを使い続けているプロジェクトがあれば、小さなストアから少しずつPiniaへ移行してみるのも良いステップです。まずは今回紹介したカウンターストアのような簡単な例から、自分のプロジェクトに組み込んでみてください。