🍍

Piniaについて、および、Piniaに移行する前に知っておきたいこと

2023/03/26に公開

この記事の要約

pinia

  • キャラかわいい
  • Vue3では、Vuexの代わりにPiniaを推奨しており、直観的・型安全・テスタブル等で利点が存在する
  • Vue3では、グローバルばデータを管理する方法は3種類提供しており、それぞれ利用方法が異なる

また、この記事の内容はSaitama.js vol.5にて、LTを行なった内容をベースにしています。
https://speakerdeck.com/2nofa11/vuexkarapiniayi-xing-nixiang-kete

Pinia とは何か、どんな特徴や利点があるか

キャラについて

最初に言わなければいけない、この愛くるしい中国製っぽいキャラ。

個人的に、オリンピックの北京2022オリンピックマスコット、「ビンドゥンドゥン」味を感じて好感を持ってます。

ビンドゥンドゥン

参考:北京 2022 オリンピックマスコット - 写真と誕生秘話 (olympics.com)

What's Pinia?

公式のトップページに記載があるとおり、The intuitive store for Vue.js(Vue.jsの直観的なストア)です。

主な特徴や利点としては、公式では以下が挙げられています。

  • 💡 直感的
  • 🔑 型セーフ
  • ⚙️ 開発ツールのサポート
  • 🔌 拡張
  • 🏗 設計によるモジュール式
  • 📦 非常に軽い

挙げられててはいるものの、百聞は一見に如かず。
実際に使ってみて動作を確認してみましょう。

そもそもストアとは?

(理解している方は読み飛ばしてください。)
ストアとは、状態をデータとして保持する場所のことです。
フロントエンド開発では、コンポーネント単位で設計を行ないますが、コンポーネント同士で値のやり取りをする必要がある場合、その状態を伝える必要があります。
そこの手段の1つとして、ストアを用います。
詳しくはフロントエンドの「状態」の入門 (zenn.dev)を参照してください。

ここでは、Vuexの紹介を説明していますが、Vuexでは4つに分類していることで構造化を実現しています。

  • ステート:状態データ
  • ゲッター:派生データ
  • ミューテーション:状態を変更する
  • アクション:Fetchなどの状態の変更以外を実行する

https://zenn.dev/shava2c/articles/ae668b689be50d

Pinia の現状

UIT Survey 2022 実施レポート (linecorp.com)にて、「Vueでの状態管理のためのライブラリの経験」の項目が存在していました。

そのなかでPiniaはリリースされてしばらくたつにも関わらず、「聞いたことがない」が半分近くいる状態です。

pinia-認知度

また、「Vueでの状態管理のためのライブラリの使用」の項目では、10%程度とVue3への移行が騒がれる中、なかなか浸透されていない状態という現実があります。

pinia-利用度

参考:UIT Survey 2022 実施レポート (linecorp.com)

Pinia の基本的な使い方やコード例

ストア側のセットアップ方法

Vue3と同様にOption StoresとSetup Storesの2つの方式が存在します。
書き心地としてはSetup Storesがいいかなと思うのですが、チームによると思います。

例えば、カウンターという状態を持つ場合は以下のようにステート、ゲッター、アクションを定義します。

  • Option Stores

    export const useCounterStore = defineStore("counter", {
      state: () => ({ count: 0, name: "Eduardo" }),
      getters: {
        doubleCount: (state) => state.count * 2,
      },
      actions: {
        increment() {
          this.count++;
        },
      },
    });
    
  • Setup Stores

    export const useCounterStore = defineStore("counter", () => {
      const count = ref<number>(0);
      const doubleCount = computed(() => count.value * 2);
      function increment() {
        count.value++;
      }
      return { count, doubleCount, increment };
    });
    

公式を見ていて移行に向けて最低限おさえていたほうがよさそうな点が2点ありました。

いなくなったミューテーション

Vuexを使用していた皆さんなら気づくと思いますが、ミューテーションがいなくなりました。
Vuexではミューテーションにてステートの値を更新していましたが、Piniaではアクションを使って値を更新します。
なお、このミューテーションの廃止は以下のメリットがあります。

  • コードがシンプルで直感的になる
  • Vueベースの記述方法で書ける

Vuex とのマイグレーションについて

公式にMigrating from Vuex ≤4として記載があります。
個人的に気を付けなければいけないな、と思うところは以下の2点です。

  • 公式ではVuex → Pinia(Option Stores) の移行方法のみ記述されています。
    Pinia(Setup Stores)の移行はStoreが肥大化していると、大変との声も聞いたことがあります。
  • 呼び出し側も少なくない変更が発生する。
    公式には呼び出し側の記載がありませんが、変更する必要があります。移行する際は呼び出し側も考えなければいけません。

テストの記述方法

Vuexのテストは以下2通りのやり方がありました。

  • global.pluginsを利用した実際のVuexでのテスト
  • global.mocksを利用したモックストアでのテスト

Piniaでは、どちらも1つの宣言createTestingPiniaで記入できます。 具体的にテストの方法を見てみましょう。

実際の Pinia を使ったテスト

以下のようにcreateTestingPiniaのオプションstubActions:falseしてあげると、テストするコンポーネントに依存するテストを実施できます。
デフォルトはtrueとなり、実際のPiniaは読み込まれずActionなどが動きません。

describe("HelloWorld - 実際のstoreを利用", () => {
  let wrapper: VueWrapper<any>;
  beforeEach(() => {
    wrapper = mount(HelloWorld, {
      global: {
        plugins: [
          createTestingPinia({
            stubActions: false,
          }),
        ],
      },
    });
  });
  test("incrementボタン押下", async () => {
    expect(wrapper.text()).toContain("count is 0");
    const button = wrapper.find("button");
    await button.trigger("click");
    expect(wrapper.text()).toContain("count is 1");
  });
});

モックストアでのテスト

モックを使う場合は、Actionのモックを定義します。
今回の場合はVitestを利用しているので、vi.spyOn(store, "increment")にてActionをモック化しています。

describe("HelloWorld - Mockでのモックstoreを利用", () => {
  let wrapper: VueWrapper<any>;
  beforeEach(() => {
    wrapper = mount(HelloWorld, {
      global: {
        plugins: [
          createTestingPinia({
            initialState: {
              counter: { count: 100, name: "foo" },
            },
          }),
        ],
      },
    });
  });
  test("incrementボタン押下", async () => {
    expect(wrapper.text()).toContain("count is 100");
    const store = useCounterStore();

    // モック化
    const incrementSpy = vi.spyOn(store, "increment");
    incrementSpy.mockImplementation(() => {
      store.count += 2;
    });

    const button = wrapper.find("button");
    await button.trigger("click");

    expect(wrapper.text()).toContain("count is 102");
  });
});

グローバルで状態を管理する方法の比較

Pinia以外に、「グローバルばデータを管理する方法」としてはVue3では2つあります。

  • provide/injectオプションを利用する
  • 状態管理ライブラリVuex

InjectとVuexとPiniaそれぞれについても比較は以下の表のとおりです。

Vue の状態管理

項目 Inject Vuex Pinia
データの範囲 親子関係のあるコンポーネント間 アプリケーション全体 アプリケーション全体
データの定義方法 provide/inject オプションを使う store オブジェクトに state や mutations などを定義する createPinia 関数で store インスタンスを作り、defineStore 関数で各 store を定義する
データの取得方法 inject オプションで取得する mapState や mapGetters などのヘルパー関数を使うか、this.$store.statethis.$store.getters などでアクセスする useStore 関数で取得する
データの更新方法 直接変更するか、emit イベントを使う mapMutations や mapActions などのヘルパー関数を使うか、this.$store.committhis.$store.dispatch などで呼び出す store 内で定義したメソッドやアクションを呼び出す

ちなみに

Bingチャットに「Piniaについて教えて」と言われたら、もっともらしいデタラメを言われました。。。

Pinia という名前は、pineapple(パイナップル)と pin(ピン)を組み合わせた造語で、パイナップルのように甘くてジューシーな状態管理を提供するという意味があります。

よく調べないといけないですね。

GitHubで編集を提案

Discussion