🍍

Nuxt3移行のついでにVuexからPiniaに切り替えた話

2023/12/20に公開

この記事はラクスパートナーズAdventCalendar2023の21日目の記事です。

はじめに

私の所属するチームではNuxtを使用しています。
NuxtはVue.jsのフレームワークで、2022年の11月に、Nuxt3がリリースされました。

Nuxt2はVue2に依存しており、Vue2は2023年12月でEOLを迎えることから、EOLを迎える前に移行を完了させることが望ましく、Nuxt3への移行は優先度高の案件でした。
ありがたいことに、Nuxt3移行を担当させていただきまして、2023年7月に移行を完了することできました🎉

今回は移行作業の1つでしたPiniaに関して書かせていただきます。

この記事の対象者

  • Nuxt3移行に関心がある方
  • NuxtでPiniaを導入したい方

なぜVuexからPiniaに切り替えた?

Vueのエコスシテムにおける、現在の状態管理ライブラリの推奨はPiniaだからです。

なぜNuxt3移行のついでとなった?

Nuxt3ではVuexとの統合を提供しなくなったので、移行作業が必要になりました。
Vuexを継続して使う場合も移行作業は必須ですので、それならこれを機にPiniaに切り替えてしまおうと考え、Nuxt3移行のついでに導入することにしました。

Piniaの導入方針

以下の2点がポイントになりました。

  • Nuxt2で導入する
  • Pinia導入前に既存のstoreをリファクタする

Nuxt2で導入する

PiniaはNuxt3移行時に導入することも可能なのですが、以下の2つを理由に、Nuxt2で導入することにしました。

① 3系(Nuxt・Vue)への移行は、破壊的変更が多い

これは3系への移行において、とても重要なポイントです。
この破壊的変更の多さを嫌って、ReactやNextにリプレイスするというケースも少なくありません。

3系に移行する場合、まず2.7系を経由して、Composition APIへの書き換えなど、3系移行前に対応可能な作業はやり切ってしまうというのが、3系移行における基本戦略と思っています。

それでも、3系移行時のビックバンリリースは避けられませんが、この作業があることで、移行時の劣化リスクを大幅に減らすことできます。

PiniaはNuxt2から使えるので、3系に移行する前に導入が可能でした。

② Pinia導入済みなら、Nuxt3への移行作業がほぼない

Nuxt3への移行に詳細がありますが、Nuxt3移行時にはほぼやることがなかったです。
この2つを理由に、Nuxt2で導入する方が明らかにメリットがあったので、Nuxt2で導入することにしました。

Pinia導入前に既存のstoreをリファクタする

既存のstoreには大量の負債がありまして、そのままPiniaを導入するのは怖さがありました。
また、新規作成したPiniaのstoreファイルに、わざわざ負債のコードを書くのも不毛な作業と思いまして、これを機に一掃することにしました。

リファクタ

既存のstoreには以下の負債がありました。

  • 使っていないstate・actions・gettersが大量にある
  • storeで管理する必要がない
  • actions・gettersのコード量と数が多すぎる

storeで管理する必要がない

特定のVueコンポーネントやComposablesで定義すれば良いコードがstoreに書かれていました。
やることはシンプルで、特定のVueコンポーネントやComposablesにコードを移動するだけです。

移動した結果、storeファイル自体削除可能なケースもあり、Pinia導入のコスト削減になりました。

actions・gettersのコード量と数が多すぎる

storeにロジックを書くということは、storeに依存する呼出元の要求を抱えることになります。
要求が揃うなら特に問題にはなりませんが、もちろん揃わないケースもあり、以下のようなことが繰り返し起きていました。

  • 特定の呼出元の要求を満たすため、actionsやgettersに、if分を書いて対応する
  • だんだん、コード量が増えてきて危機感を覚える
  • たまらず、新しいactions・gettersを作成する

storeに限らずですが、このようなコードを見たことがあるのではないでしょうか?
これの繰り返しにより、storeがカオスになってしまいました。

この負債に対処するため、私のチームではstoreにロジックを書くのを基本NGにしました。
つまり、storeでできることは、stateの参照と更新だけです。

なので、actionsとgettersのコードは基本全て移動しました。
具体的には、対象のactionsやgettersの呼出元が1箇所しかないなら、Vueコンポーネントにそれらのコードを移動します。
呼出元が複数のケースでは、Composables内でstoreに参照し、ロジックやcomputedを提供すれば解決できます。

Nuxt2でPinia導入

ここまでで、storeのリファクタは完了しているので、導入はとても楽になります。
やることは以下の2つです。

  • install・setup
  • VuexからPiniaへの書き換え

install・setup

ドキュメントに従い、Nuxt2でPiniaのinstallとsetupを行います。

Piniaのinstall

yarn add pinia @pinia/nuxt@0.2.1

nuxt.config.tsの更新

export default {
  // ... other options
  buildModules: [
    '@pinia/nuxt',
  ],
}

tsconfig.jsonの更新

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

VuexからPiniaへの書き換え

詳細はマイグレーションガイドに譲りますが、作業は以下の4つでした。

① storesフォルダを作成して、そこにstoreファイルを配置する

Restructuring Modules to Storesに詳細がありますが、Vuexとの設計の違いや、移行を段階的に行えるようにするため、フォルダ名がVuexと異なるようです。

そのため、新規にstoresフォルダを作成して、そこにPiniaのstoreファイルを置きます。

② Composition APIでstoreファイルを実装

Piniaは「Options API・Composition API」のどちらでも実装可能です。
私のチームでは、Piniaに限らずComposition APIで実装していく方針でしたので、Composition APIで書くことにしました。

マイグレーションガイドではOptions APIへの移行例しかありませんが、Composition APIでの書き方はCore Conceptsで確認できます。

③ storeの呼出元の書き換え

VuexとPiniaではstoreの参照方法が異なります。
修正箇所はそれなりになりますが、単純作業ですので、そこまで大変ではなかったです。
私のチームでは、modeはuniversalと、ページ遷移はSSRでしたので、コンポーネント外での利用時は、Piniaインスタンス($pinia)が必要と、そこだけ注意が必要でした。

Piniaの参照方法はUsing the storeで確認できます。

④ Testの書き換え

私のチームでは、Vitest(移行当時はJest)とTesting Libraryでテストを書いています。
テスト対象のファイルがstoreに依存している場合、テストファイルでPiniaインスタンスを作成する必要があります。

// stores/counter.spec.ts
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '../src/stores/counter'

describe('Counter Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia()) // この宣言のみでOK
  })
  // ...
})

テスト実行前にstateを変更したい場合は以下のように書きます。
Vuexでmockを書くのはとても面倒でしたが、Piniaではとても簡単でした。

// stores/counter.spec.ts
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '../src/stores/counter'

describe('Counter Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })
  
  it('sample', () => {
    const counter = useCounterStore()
    expect(counter.n).toBe(0)
    counter.n = 1 // これでstateが更新されます
    expect(counter.n).toBe(1) // これはpassになります
  })
})

Nuxt3への移行

Piniaの移行作業は以下の2つと、ほぼ作業がなかったです。

  • installのバージョンとconfigの変更
  • middleware・pluginでのstore宣言の修正

installのバージョンとconfigの変更

ドキュメントに従い、Nuxt3では@pinia/nuxtの最新バージョンでinstallします。

yarn remove @pinia/nuxt && yarn add @pinia/nuxt

または

yarn upgrade --latest @pinia/nuxt

tsconfig.jsonですが、Nuxt3では以下を削除できます。

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

middleware・pluginでのstore宣言の修正

store宣言時にPiniaインスタンス($pinia)を渡す必要がなくなるので削除します。

Nuxt3移行時の作業はこれだけでした!

最後に

最後まで読んでいただき、ありがとうございました!

技術ブログというものを初めて書いてみましたが、記事を書くために当時の作業を振り返ったり、ドキュメントを再確認したりと、大変勉強になりました🙇‍♂️

Nuxt3移行のネタは大量にありまして、私の拙い文章でも誰かの役に立つかもしれないので、他のネタも記事にしようと思います🙏
この記事も、誰かの役に立てれば幸いです!

参考文献

https://pinia.vuejs.org/
https://v2.nuxt.com/
https://nuxt.com/
https://v2.vuejs.org/
https://vuejs.org/
https://github.com/nuxt/bridge
https://github.com/nuxt-community/composition-api
https://github.com/vuejs/composition-api
https://speakerdeck.com/tooppoo/what-should-do-or-not-with-vuex

Discussion