11
Help us understand the problem. What are the problem?

posted at

updated at

Nuxtプロジェクトの状態管理をPiniaに移行してみた

この記事は 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へ以下を追加します。

nuxt.config.js
buildModules: [
  '@nuxtjs/composition-api/module',
  '@pinia/nuxt', // 追加
],

Vuexと共存させたい場合はdisableVuex: falseを指定するとOKです。

nuxt.config.js
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を追加してコンパイル時に参照する型定義ファイルを指定します。これでオートコンプリートが効いてさくさく開発ができるようになります🎉

tsconfig.json
{
  "types": [
    // ...
    "@pinia/nuxt"
  ]
}

Storeを定義する

インストールとセットアップができたのでStoreを定義していきます。
わたしのアプリではアニメのシーズン名をサイドバーや複数コンポーネントから参照する必要があったため、Storeで管理をしています。

store/season.ts
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にカーソルを当ててみると、、
スクリーンショット 2021-12-11 12.54.57.png
ばっちり型推論されてます。

もちろんsetSeasonNameText()にnumberを渡そうとしたらコンパイルエラーになってくれます。優勝。
スクリーンショット 2021-12-11 12.59.09.png

番外編 永続化しておく

リロードしてStoreが消えてしまうと困るアプリだったので永続化もしておきます。
pinia-plugin-persistという素晴らしいライブラリがあったため使わせていただきます。GitHubに記載されてる通り、まだ開発中のため使用する際は自己責任です。

インストール&セットアップ

まずはパッケージ追加。

npm i pinia-plugin-persist

次にpluginを追加します。Piniaのインターフェースでプラグインを追加するためのuse関数が用意されているため、それにpiniaPersistを渡します。

pinia-plugin-persist.client.ts
import { Context } from '@nuxt/types'
import piniaPersist from 'pinia-plugin-persist'

export default ({ app }: Context) => {
  app.pinia?.use(piniaPersist)
}

piniaPersistでは以下のようにアンビエントモジュールでPersistOptionsという型情報が付加されているため、Piniaでpersistオブジェクトが使用できるようになります。

pinia-plugin-persist.d.ts
declare module 'pinia' {
  interface DefineStoreOptions<Id extends string, S extends StateTree, G extends GettersTree<S>, A> {
      persist?: PersistOptions;
  }
}

nuxt.config.jsでプラグインを有効にします。

nuxt.comfig.js
plugins: [
  // ...
  '@/plugins/pinia-plugin-persist.client'
],

tsconfig.jsonにも型情報を渡します。

tsconfig.json
{
  "types": [
    // ...
    "pinia-plugin-persist"
  ]
}

Storeで永続化を有効にする

最後はpersistオブジェクトでenabled: trueを指定することで永続化が有効になります。保存先のストレージのデフォルトはセッションストレージなので、用途によって変更することができます。わたしはローカルストレージを使うようにしました。

store/season.ts
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のリリースが楽しみです!
最後まで読んでいただきありがとうございました。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
11
Help us understand the problem. What are the problem?