Piniaとは
PiniaはVuejs向けの状態管理ライブラリだ。コンポーネント間やページ間でグローバルに状態共有することができる。
VuejsのComposition APIを意識して実装されているものの、OptionsAPIやVue2に適用することも可能だ。
Vuex5.xのRFCの要求事項をほとんど満たしていたということもあって、Vuexに代わってVue公式の状態管理ライブラリに指名されている。すなわち、比較的新しいライブラリながらも本番案件に利用できるだけの機能が十分に備わっているといえる。
この記事では取り上げないが、サーバーサイドレンダリング(ViteやNuxt.js)にも対応している。
ちなみに、発音は「ピーニャ」だ(発音記号で書けば/piːnjʌ/
)。
この記事で解説すること
Pinia公式ドキュメントに倣いつつ、以下の項目について解説する。
- インストール方法
- 簡単な利用例
- Store、State、Getters、Actionsの詳しい使い方
インストール
以下コマンドで導入可能。
yarn add pinia
npm install --dev pinia
試してみる
PiniaをVueアプリインスタンスに適用
以下のように、PiniaインスタンスをApp.use()
の引数に入れればVueアプリでPiniaが使用可能になる。
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
createApp(App).use(pinia).mount('#app')
Storeの作成
まずはdefineStore()
を使用してStoreを作成する。
defineStore()
の第一引数にはStoreの一意なIDを付け、第二引数にStoreの定義を書く。
そしてdefineStore()
の戻り値をエクスポートし、他のモジュールで使うことになる。
import { defineStore } from 'pinia'
export const useMyStore = defineStore('myStore', {
//保持したいデータ
state: () => {
return {
count: 0,
}
},
getters: {
double: (state) => {
return state.count * 2
},
},
actions: {
increment() {
this.count++
},
},
})
PiniaのStoreには
- state
- getters
- actions
の3つのプロパティを定義できる。
stateにはデータの読み書き、gettersには加工したデータの読み取り、actionsには各種メソッドを実装していく。
これらはVuejsのdata, computed, methodsに相当するものと考えればわかりやすい。
他にもwatchに相当するものとして、Stateの変更やActionsの呼び出しの監視機能がある。
コンポーネントからの利用
PiniaのStoreをコンポーネントで使う場合の例が以下になる。
記述量が減るので、<script setup>
での利用を推奨。
<template>
<div class="hello">
<button @click="myStore.increment">increment</button>
<p>Count: {{ myStore.count }}</p>
<p>DoubleCount: {{ myStore.double }}</p>
<p>Count(String): {{ countString }}</p>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useMyStore } from '@/myStore'
const myStore = useMyStore()
const countString = computed(() => {
return String(myStore.count)
})
</script>
script setup
を利用しない場合は以下のようになる。
注意点として、useMyStore()
はsetup()
関数内で実行しなくてはならない。
理由は後述の「コンポーネント外でStoreを使う」を参照のこと。
<template>
<div class="hello">
<button @click="myStore.increment">increment</button>
<p>Count: {{ myStore.count }}</p>
<p>DoubleCount: {{ myStore.double }}</p>
<p>Count(String): {{ countString }}</p>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { useMyStore } from '@/myStore'
export default defineComponent({
setup() {
const myStore = useMyStore()
const countString = computed(() => {
return String(myStore.count)
})
return {
computed,
myStore,
countString,
}
},
})
</script>
コンポーネントからStoreを利用するには、まず先ほどエクスポートしたuseXXX
をインポートし、関数として実行した戻り値をStoreとして扱う。
import { useMyStore } from '@/myStore'
const myStore = useMyStore()
あとはこのStoreに登録された各stateなどを呼び出せばよい。
// stateの読み取り
const count = myStore.count
// stateの更新
myStore.count = 5
// gettersにアクセス
const doubledCount = myStore.double
// actionsを実行
myStore.increment()
当然ながら、違うファイル内でも同じStoreを呼び出せば値は共有される。
import { useMyStore } from '@/myStore'
const myStore = useMyStore()
console.log(myStore.count) // 5
Vuexと比べると、state
の読み取りに.state
を挟む必要がない、store.commit()
無しで状態更新ができるなど、構文が短くなっている。
以上のように、かなりスッキリとした構文でStoreを利用することができる。
なお、非同期処理関数を使いたい場合には後述のようにActionsで実装することになる。Gettersには非同期処理関数を登録できないので注意。
より詳しい使い方
Store
命名規則
defineStore
の戻り値の関数はComposableな関数にあたる。
CompositionAPIの慣習に則り、Pinia公式では戻り値の名前をuseXXX
の形式にすることを推奨している。
(参考:https://pinia.vuejs.org/core-concepts/)
コンポーネント外でStoreを使う
PiniaのStoreはPiniaインスタンスに依存している。
そのため、Piniaインスタンスが準備されていない環境でuseMyStore
などを呼び出すとエラーになる。
useMyStore()
// getActivePinia was called with no active Pinia. Did you forget to install pinia?
// const pinia = createPinia()
// app.use(pinia)
Storeのテストを行うなど、Component外でStoreを利用したい場合は、useMyStore()
にPiniaインスタンスを注入すればよい。
方法としてはPiniaインスタンスを作って注入する方法と、setActivePinia()
を使う方法の2通りがある。
import { setActivePinia, createPinia } from 'pinia'
import { useMyStore } from './myStore'
// 方法1:Piniaインスタンスを作って普通に注入
const pinia = createPinia()
const myStore = useMyStore(pinia)
// 方法2:setActivePinia()を利用
setActivePinia(createPinia())
const myStore = useMyStore()
複数のStore
Piniaで複数のStoreを使いたい場合は、それぞれを別のファイルに定義する。
Store間で連携が必要になった場合でも、useXXX()
でStoreを呼び出せばそのstateなどを利用できる。
とはいえ、2つのStoreが相互に参照しあうようであれば1つに統合した方がよい。
以下は、myStore
の他にanotherStore
を作成し、その中でmyStore
のstateを呼び出す例。
import { defineStore } from 'pinia'
import { useMyStore } from './myStore'
export const useAnotherStore = defineStore('anotherStore', {
state: () => {
return {
text: '',
}
},
getters: {
textPlusCount: (state) => {
const myStore = useMyStore()
// myStoreのstate呼び出し
return state.text + myStore.count
// getters呼び出し
// myStore.double
// actions呼び出し
// myStore.increment()
},
},
})
リアクティブ性を保ちつつスプレッド構文を使う
PiniaのStoreはreactive
でラップされたオブジェクトなので、スプレッド構文で展開すると、StateやGettersのリアクティブ性を壊してしまう。
const myStore = useMyStore()
// ❌ リアクティブ性が壊れてしまう = 変更を検知できない
const { name, doubleCount } = myStore
リアクティブ性を保つには、storeToRefs()
を用いるとよい。
なお、actionsはそのままスプレッド構文を用いても問題ない。
import { storeToRefs } from 'pinia'
const store = useMyStore()
// リアクティブな参照になる
const { name, doubleCount } = storeToRefs(myStore)
// actionsはそのままスプレッド構文を用いてもOK
const { increment } = myStore
})
State
Stateへのアクセス
Storeに定義したStateは、基本的に直接アクセス可能。例えば、下記のように加算演算子を使っても問題なく動作する。
myStore.counter++
Stateのリセット
store.$reset()
を呼び出すことで、Stateを定義した初期状態にリセットすることができる。
myStore.count = 100
// ...
// countの値が、Store定義時にStateとして設定した値に戻る。
// この場合は0になる。
myStore.$reset()
Stateの変更
Stateを変更する際、Piniaではstore.counter = 1
やstore.counter++
のように直接操作できることを既に学んだ。
この他に、store.$patch
メソッドを呼んで変更する方法をとることもできる。
このメソッドは、変更後のStateを直接引数に入れるのと、stateを引数にとるコールバックを引数に入れる使い方の2通りが用意されている。
基本的には前者でよいが、配列の操作など変更内容が複雑な場合には後者を使うとよい。
// オブジェクトを代入
myStore.$patch({
count: myStore.count + 100
})
// コールバック
myStore.$patch((state) => {
state.count = state.count + 100
})
State全体の置き換え
あるStoreのState全体を置き換える操作は、store.$state
に直接代入することで実行できる。
myStore.$state = {
count: 24,
isCountStop: true,
}
さらに、Piniaインスタンスが持つすべてのStateを置き換えるには次のようにすればよい。
(サーバーサイドレンダリングのState hydrationに使うらしい)
pinia.state.value = {}
Stateの変更監視
$subscribe()
メソッドを使うことで、Stateに変更があった場合に行いたい処理を設定できる。引数にはmutation
とstate
を引数にとるコールバックを登録する。
-
mutation.type
は、前述のStateの変更時に使った方法によって異なる値になる。直接代入なら'direct'
、$patch()
の引数にオブジェクトを入れれば'patch object'
、関数を入れれば'patch function'
になる。 -
mutation.storeId
は、Storeの定義時に設定したStoreのIDを格納している。 -
mutation.payload
はmutation.type
が'patch object'
の時にのみ、$patch()
の引数に代入したオブジェクトを格納している。
// mutation.typeの型
// import { MutationType } from 'pinia'
myStore.$subscribe((mutation, state) => {
console.log(mutation.type) // 'direct' | 'patch object' | 'patch function'
// myStore.$idと同じもの
console.log((mutation.storeId) // 'myStore'
// mutation.type === 'patch object'の時のみ有効
console.log(mutation.payload) // myStore.$patch()に渡したオブジェクト
// Stateに変更があったときに実行したい処理を書いていく
// ...
})
Getters
同じStore内の他のGettersにアクセス
同じStore内の他のGettersの戻り値を参照したい場合は、this
を使ってアクセスする。
その場合
- アロー関数は使うことができない(
this
がundefined
になるため) -
state
にアクセスしない場合、引数のstate
も取り除く(linter設定しだいではエラーになる)
に気を付けたい。
export const useMyStore = defineStore('myStore', {
state: () => {
return {
count: 0,
}
},
getters: {
double: (state) => {
return state.count * 2
},
// アロー関数はthisがundefinedになる可能性があるため使えない
doublePlusOne(): number {
// thisでアクセスする
return this.double + 1
},
},
})
Gettersに引数を渡す
Gettersに引数を渡したい場合、Gettersの戻り値を値ではなく関数にする。
その関数の引数には、Gettersに渡したい引数を設定する。
export const useMyStore = defineStore('myStore', {
state: () => {
return {
count: 0,
}
},
getters: {
double: (state) => {
return state.count * 2
},
multiplied: (state) => {
// 関数を返すようにする
return (num: number) => state.count * num
}
},
})
呼び出し側では通常の関数のように利用できる。
// 呼び出し
myStore.multiplied(5)
Actions
非同期処理を行うActions
PiniaのActionsには、非同期処理を実行する関数も登録できる。
通常の非同期処理関数のようにasync
キーワードを使えばよい。
export const useMyStore = defineStore('myStore', {
state: () => {
return {
count: 0,
}
},
actions: {
async fetchCount(){
const fetchedCount = await fetchXxxValue()
this.count = fetchedCount
}
},
})
Actionsの呼び出しを監視する
$onAction
メソッドを使うと、PiniaのストアのActionsが実行されるタイミングで別の処理を実行させることができる。
$onAction
の引数に実行したい処理を行うコールバックを代入する。
処理を実行させたいタイミング(action実行前、実行後、例外発生時)に応じて、次の3か所に処理を実装していくことになる。下記のサンプルコードも参照のこと。
- action実行前:
$onAction
に渡すコールバック内 - action実行後:
after
に渡すコールバック内 - 例外発生時:
onError
に渡すコールバック内
myStore.$onAction(
({
name, // 実行されたactionの名前
store, // Storeのインスタンス。`myStore`に同じ
args, // actionに渡された引数の配列
after, // actionの完了後(Promiseであれば`resolve()`された後)に実行する処理
onError, // actionで例外が投げられたとき(Promiseであれば`reject()`されたとき)に実行する処理
}) => {
// action実行前に行われる処理
console.log({
name,
store,
args,
})
after((result) => {
// action実行後に行われる処理
console.log(result)
})
onError((error) => {
// actionでエラーが発生したときに行われる処理
console.error(error)
})
}
)
なお、この機能ではStoreのActions実行を監視できるが、コールバックはStore内のすべてのActionsに対して実行される。すなわち、個別のactionのみを直接監視することはできない。
その場合は以下のようにaction実行前に行う処理の中でactionの名前(name
)をチェックし、後続処理を続けるか分岐させるとよい。
// action実行前に行われる処理
// `someAction`以外は無視
if(name !== 'someAction') return
さらなる機能として、上記で登録したコールバックの実行を止めさせることもできる。
そのためには$onAction
の戻り値を受け取っておき、止めさせたいタイミングで実行する。
const unsubscribe = myStore.$onAction(/* 略 */)
// 上で登録したコールバックを登録解除する
unsubscribe()
まとめ
Vuejsの新たな状態管理ライブラリであるPiniaについて、以下の点を紹介した。
- インストール方法
- 簡単な利用例
- Store、State、Getters、Actionsの詳しい使い方
この記事を参考にしながら、ぜひPiniaを使ってみてほしい。
参考
Pinia公式ガイド https://pinia.vuejs.org/introduction.html
公式ガイドにはこの記事では紹介しなかったトピック(サーバーサイドレンダリング対応、Vuex≦4からの移行など)も紹介されているため、必要になった場合は参照してほしい。