この記事は Sansan Advent Calendar 2021 の14日目の記事です。
今回は気になっていたPiniaを触ってみるべく、個人開発しているWebアプリの状態管理をPiniaに移行してみたのでその手順と所感をまとめたいと思います。
ソースコードは以下です。
環境, 前提
- Node.js 16.13.0
- npm 8.1.4
- Nuxt.js 2.15.8 (TypeScript + Composition API)
Piniaについて
PiniaはVueのグローバルステートマネジメントのためのライブラリです。
Vueの状態管理といえばVuexが主流ですが、PiniaはComposition APIにおけるStoreの扱いを再設計するための実験として、Vue.jsコアチームメンバーのposvaさんによって作成されたようです。
そしてVuex5は現在RFCにて絶賛仕様検討, 開発中でPinia作成者であるposvaさんも一緒に作っていて、Piniaからインスパイアを大いに受けているとのことです。
https://github.com/vuejs/rfcs/discussions/270
Pinia, Vuex5の特徴としては
- Mutationsが廃止され、脱Fluxしている
- 完全なTSサポート
- ModuleによるStoreのネストの廃止
などが挙げられます。
これまでのVuexでは状態管理したいだけなのに、まずはFluxの理解からしなければならないのが開発者にとって壁になってなっていた(私はそうだった)ので、サクッと使えるのはとても嬉しいです。また自前で型を拡張せずとも型が効いてくれるのもとってもありがたいです。
インストール
ではさっそくPiniaを入れていきます。公式の手順通り、Pinia本体とNuxt用のモジュールを追加します。
npm i pinia @pinia/nuxt
nuxt.config.jsのbuildModules
へ以下を追加します。
buildModules: [
'@nuxtjs/composition-api/module',
'@pinia/nuxt', // 追加
],
Vuexと共存させたい場合はdisableVuex: false
を指定するとOKです。
buildModules: [
'@nuxtjs/composition-api/module',
['@pinia/nuxt', { disableVuex: false }],
],
しかしドキュメントには
https://pinia.esm.dev/ssr/nuxt.html#using-pinia-alongside-vuex
It is recommended to avoid using both Pinia and Vuex
と書かれてるので移行完了したらVuexは消すのが良さそうです。
続いて、tsconfig.jsonのtypes
に@pinia/nuxt
を追加してコンパイル時に参照する型定義ファイルを指定します。これでオートコンプリートが効いてさくさく開発ができるようになります🎉
{
"types": [
// ...
"@pinia/nuxt"
]
}
Storeを定義する
インストールとセットアップができたのでStoreを定義していきます。
わたしのアプリではアニメのシーズン名をサイドバーや複数コンポーネントから参照する必要があったため、Storeで管理をしています。
import { defineStore } from 'pinia'
export const useSeason = defineStore('season', {
state: () => ({
seasonNameText: ''
}),
actions: {
setSeasonNameText(val: string) {
this.seasonNameText = val
}
}
})
defineStore
関数でStoreを定義します。第一引数はユニークな名前にする必要があり、これはidとも呼ばれます。StoreをDevtoolsに接続する際に必要となるみたいです。そして、第2引数のoptionsでstateやgetters, actionsを定義することができます。
またexportする関数名はuseXXXX
と命名するのが慣習だと書かれてたのでそれに従います。
https://pinia.esm.dev/core-concepts/#defining-a-store
Naming the returned function use... is a convention across composables to make its usage idiomatic.
今回はactionsにsetSeasonNameText()
という関数だけ追加してますが、actionsから直接stateを更新できるのは直感的で良いですね!
コンポーネントからStoreを参照する
作ったStoreをコンポーネントから呼び出します。(関係ある部分だけ抜粋してます。)
<template>
<v-list-item @click="season.setSeasonNameText(item.seasonNameText)">
<v-list-item-icon />
</v-list-item>
</template>
<script lang="ts">
import { defineComponent, SetupContext } from '@vue/composition-api'
import { useSeason } from '@/store/season'
export default defineComponent({
setup(_props, context: SetupContext) {
// ...
const season = useSeason()
return {
season
}
}
})
</script>
先ほど定義したuseSeason()
を呼び出しStoreインスタンスをseason
に代入し、returnしてテンプレート内で使用できるようにします。
season
にカーソルを当ててみると、、
ばっちり型推論されてます。
もちろんsetSeasonNameText()
にnumberを渡そうとしたらコンパイルエラーになってくれます。優勝。
番外編 永続化しておく
リロードしてStoreが消えてしまうと困るアプリだったので永続化もしておきます。
pinia-plugin-persist
という素晴らしいライブラリがあったため使わせていただきます。GitHubに記載されてる通り、まだ開発中のため使用する際は自己責任です。
インストール&セットアップ
まずはパッケージ追加。
npm i pinia-plugin-persist
次にpluginを追加します。Piniaのインターフェースでプラグインを追加するためのuse
関数が用意されているため、それにpiniaPersist
を渡します。
import { Context } from '@nuxt/types'
import piniaPersist from 'pinia-plugin-persist'
export default ({ app }: Context) => {
app.pinia?.use(piniaPersist)
}
piniaPersist
では以下のようにアンビエントモジュールでPersistOptions
という型情報が付加されているため、Piniaでpersist
オブジェクトが使用できるようになります。
declare module 'pinia' {
interface DefineStoreOptions<Id extends string, S extends StateTree, G extends GettersTree<S>, A> {
persist?: PersistOptions;
}
}
nuxt.config.js
でプラグインを有効にします。
plugins: [
// ...
'@/plugins/pinia-plugin-persist.client'
],
tsconfig.json
にも型情報を渡します。
{
"types": [
// ...
"pinia-plugin-persist"
]
}
Storeで永続化を有効にする
最後はpersistオブジェクトでenabled: true
を指定することで永続化が有効になります。保存先のストレージのデフォルトはセッションストレージなので、用途によって変更することができます。わたしはローカルストレージを使うようにしました。
import { defineStore } from 'pinia'
export const useSeason = defineStore({
id: 'season',
state: () => ({
seasonNameText: ''
}),
actions: {
setSeasonNameText(val: string) {
this.seasonNameText = val
}
},
// 追加
persist: {
enabled: true,
strategies: [
{ storage: localStorage }
]
}
})
これでPiniaでStoreの永続化も実現できました🎉
まとめ
いままでのVuexでつらみポイントだったFluxパターンの重厚さや、型付けの課題がすっきり解消されてとっても使いやすく感じました。
今回移行したアプリは一部分でしかVuexを使用していなかったこともあり簡単にPiniaに差し替えることができましたが、(公式の推奨ではないものの)Vuex, Piniaの共存もできるので少しづつ移行していくといったことも可能そうです。業務でも使う機会があったら挑戦してみたいと思います。
今後のPiniaのアップデートとVuex5のリリースが楽しみです!
最後まで読んでいただきありがとうございました。