【Pinia】Vue3標準状態管理ツールPinia入門

![giphy (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/01e7e6d6-3d29-0814-7839-6499c4033b2e.gif)

Vue3のリリースから、Vuexに取って代わる状態管理ツールとして誕生した**Pinia**について、Vue2までメジャーだったVuexとの比較を交えながら使用方法を解説していきたいと思います。
本記事のコードサンプルは、Vue3のCompositionAPIで記述しています。「Vue2は分かるけど、Vue3はあんまり...」「CompositionAPI...?」という方は[こちらの記事](https://qiita.com/whopper1962/items/21caee0c649cd0a8151c)でVue3/CompositionAPIについて解説していますので、是非ご覧ください！✨

## Piniaとは
![pinia.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/c8bfd779-dd5f-694d-6397-925ccfde9992.png)
Vue.jsのコアチームの1人でVueRouterの開発者でもある[Eduardo San Martin Morote](https://github.com/posva)によって開発が進められ、2019年にリリースされたコンパクトで使いやすい状態管理ツールです。
Vue2まではVuexが公式で推奨された状態管理ツールでしたが、Vue3からはPiniaを使用することが推奨されています。
発音は「ピニア」で、スペイン語でパイナップルという意味です。パイナップルが複数の花から構成される1つの果実であることが、見た目は1つの大きなストアですが実際には機能毎に細かく切り分けられるストアの構造が連想されるからだそうです（参考▶︎[Why Pinia](https://pinia.vuejs.org/introduction.html#why-pinia)）。ロゴが話題のJSつよつよランタイム[Bun](https://bun.sh/)並みに可愛いです。

## Vuexとの比較

### TypeScriptとの相性Up
Vuexは、デフォルトでは型推論がサポートされていなかった為、型推論を有効にする為に一手間必要だったのですが、Piniaではデフォルトの状態で型推論までTypeScriptをフルサポートしています。

### mutationsの廃止
Vuexを使用したことがある方は`mutations`に馴染みがあると思いますが、PiniaではVuexの`mutations`にあたる機能は存在しません。
Stateの値はMutationsでのみ行われるということを担保されるというのがMutationの良点とされていました。しかし、Pinia開発者Eduardoは「これ...いらなくない...?」となりました。その結果、Piniaでは廃止されています。
Mutationsに二度手間感を抱いていたプログラマーは結構いるのでは無いかと思います。僕もその1人でしたので、この廃止はストアをよりシンプルに、使い易くしたという印象を受けました。

## インストール&初期設定手順
以下コマンドでPiniaをインストールします。
~~~bash
npm install pinia
~~~
`src/main.js`（または`src/main.ts`）を以下にように編集します。
初期化メソッド`createPinia()`をインポート/実行し、その実行結果を`app.use()`に渡すことでアプリケーションにPiniaを読み込ませます。
~~~diff_javascript:src/main.js
import { createApp } from 'vue';
+ import { createPinia } from 'pinia';
import App from './App.vue';
import './style.css';

const app = createApp(App);

+ const pinia = createPinia();
+ app.use(pinia);

app.mount('#app');
~~~
これでインストールは完了です！[Vueのデバッグツール](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd?hl=en)をブラウザにインストールしている方は、Piniaのタブが追加されていることが確認できると思います。（▶︎参考：[VueDevtoolsをChromeにインストールする方法](https://johobase.com/vue-js-devtools-install/)）
![Screen Shot 2023-03-26 at 14.50.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/08a1d960-ffb7-8932-aecb-f54fc8e966fd.png)
## 使用方法：ストア側
### 🍍ストアの定義
`defineStore`メソッドを使用することで、ストアを定義することができます。このメソッドの返り値を変数に関数として格納し、それを実行することでコンポーネントからストアを利用するという流れになります。この関数は`use~Store`という名前を付けるのが一般的です。ストアを利用する為の関数という意味合です。
`defineStore`メソッドには、第一引数にストアのID、第二引数で実際のストアを定義するオブジェクトを指定します。第一引数にストアオブジェクトを渡して、その中のオプション`id`でストアのIDを指定することも可能です。
一般的には、`src`ディレクトリ配下に`store`ディレクトリを作成し、そこにストアのファイルを機能毎に分けて配置します。注意点として、第一引数に指定するIDは他のストアと重複しないものを指定する必要があります。重複する場合、エラーが出力されます。
~~~javascript:src/store/counter.js
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  // ストアオプション
});

// または

export const useCounterStore = defineStore({
  id: 'counter',
  // ストアオプション
});
~~~

### 🍍state
ストアの核となる領域です。管理するデータと、その初期値を定義することができます。
~~~diff_javascript
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
+  state: () => ({
+    count: 0
+  }),
});
~~~

### 🍍getters
`state`のデータを参照する算出プロパティを定義する領域です。
Piniaでは`state`を直接参照することもできますが、`state`のデータに一手間加えた形で取得したい場合は、`getters`にメソッドを追加します。
ゲッターメソッドの中で`state`にアクセスする際は、メソッドの引数に`state`を取り、`state.{STATE_NAME}`という感じでアクセスします。
また、ゲッターメソッドの中で他のゲッターメソッドを参照する場合は、`this.{GETTER_NAME}`という感じで使用します。
~~~diff_javascript
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
+  getters: {
+    doubleCount(state) {
+      return state.count * 2;
+    },
+    doublePlusOne() {
+      return this.doubleCount + 1;
+    }
+  }
});
~~~

### 🍍actions
`state`の値を更新するメソッドを定義することができます。
アクションメソッドの中で`state`にアクセスする際は、`this.{STATE_NAME}`という感じでアクセスします。
~~~diff_javascript
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  getters: {
    doubleCount(state) {
      return state.count * 2;
    },
    doublePlusOne() {
      return this.doubleCount + 1;
    }
  },
+  actions: {
+    countUp() {
+      this.count++;
+    }
+  }
});
~~~

## 使用方法：コンポーネント側
定義した関数をストアからインポートし、その実行結果を変数に入れることでコンポーネントにストアを取り込むことができます。その変数を通してStateの参照、Gettersの参照、Actionsの実行を行うことができます。
~~~vue
<template>
  <div>
    <p>Count: {{ counterStore.count }}</p>
    <p>Doubled Count: {{ counterStore.doubleCount }}</p>
    <button @click="counterStore.countUp()">Count up!</button>
  </div>
</template>

<script setup>
import { useCounterStore } from '../store/count.js';
const counterStore = useCounterStore();
</script>
~~~
また、VueDevtoolsをインストールしている方は、この時点でPiniaのデバッグツールに`counter`（ストア定義時にIDに指定した値）が追加されていると思います。これを利用することで、ストアの状態を視覚的に把握できるようになります。
![Screen Shot 2023-03-27 at 7.36.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/0ccaac41-5919-a596-928e-bfbef79f3dab.png)

### 🍍`storeToRefs()`
たとえば以下のように実装して、Stateの値を分割代入することが可能に思えます。Actionsはこれでも機能するのですが、Stateを参照している値がリアクティブになりません。（`counterStore.countUp()`が実行されても画面に表示される`Count:`と`Doubled Count:`の値が更新されなくなる）
~~~javascript
const { count, doubleCount, countUp } = useCounterStore();
~~~
分割代入でStateの値を変数として定義する場合は、以下のようにPiniaが提供している`storeToRefs()`というメソッドを使用しましょう。
~~~vue
<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Doubled Count: {{ doubleCount }}</p>
    <button @click="countUp()">Count up!</button>
  </div>
</template>

<script setup>
import { useCounterStore } from '../store/count.js';
import { storeToRefs } from 'pinia';

const counterStore = useCounterStore();
// state
const { count, doubleCount } = storeToRefs(counterStore);
// actions
const { countUp } = counterStore;
</script>
~~~

### 🍍`$reset()`
Piniaではストアの`state`の状態をリセットする`$reset()`というメソッドがサポートされています。VuexではStateの値を初期化する為にはActionsに初期化アクションを新たに追加する必要があった為、Piniaの`$reset()`はなかなか便利な機能です。
~~~diff_vue
<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Doubled Count: {{ doubleCount }}</p>
    <button @click="countUp()">Count up!</button>
+   <button @click="counterStore.$reset()">Reset Store🗑</button>
  </div>
</template>

<script setup>
import { useCounterStore } from '../store/count.js';
import { storeToRefs } from 'pinia';

const counterStore = useCounterStore();
// state
const { count, doubleCount } = storeToRefs(counterStore);
// actions
const { countUp } = counterStore;
</script>
~~~