【Vue.js】Piniaとは/使い方

# Piniaとは

Vue3からVuexに代わる状態管理ツールとしてPiniaというStoreライプらりが導入されました。PiniaではVuex5向けのアイデアをいろいろ盛り込み、現在のVuexで不足している部分が改修されています。

Vue.jsのコアチームの一人、VueRouterの開発者でもあるEduardo San Martin Moroteによって開発されました。

< Pinia公式サイト >

https://pinia.vuejs.org/

# Vuexとの違いについて

私はVuexを触っていないので、詳しく分かりませんが、ザっと調べた感じ以下の違いがあるようです。

・mutationの廃止
・Typescriptのサポート
・ネストされたモジュールの廃止
・ネームスペースの廃止

# 導入

## インストール
```
npm install pinia
```

## セットアップ

```js:main.js
import { createApp } from 'vue'
import { createPinia } from "pinia";
import App from './App.vue'

import './assets/style/tailwind.css'


const app = createApp(App);
app.use(createPinia());
app.mount('#app');
```


# ストアを定義

VueにPiniaをセットアップしたら、データを格納するストアを定義します。
通常は、src/stores/ディレクトに保存します。

ストアはdefineStore()を使って定義し、名前を付けてエクスポートします。

※複数のストアを定義できますが、各ストアはそれぞれファイルを分けて定義する必要があります。

```js:store/recipe.js
import { defineStore } from "pinia";

export const useRecipeStore = defineStore('recipeStore', {
    state: () => ({
        recipes: [],
        recipe: {
            id: 1,
            title: "First recipe",
            time: 20,
            price: 500,
            filename: "20230531_cNj3iaOi7Gq8xUZ.jpg"
        }
    })
})
```

### defineStore

defineStore()は2つの引数を受け取ります。

| 引数 | |
| --- | --- |
|id|ストアを識別するための一意な名前|
|options|ストアを定義するためのオプション。OptionsオブジェクトまたはSetip関数のいずれかを受け取ります。|

defineStore()は定義されたストアのインスタンスを生成する関数を返します。

※defineStore()の戻り値の関数は、コンストラクタではなくgetter関数です。

戻り値のgetter関数はコンポーネント側から参照できるように名前ついエクスポートします。

# ストアにアクセス

定義したストアはコンポーネントのsteup()内で呼び出されるまで作成されません。
コンポーネント側でストアにアクセスするには、定義したストアをインポートし、setup内でインポートしたストアの関数useXxxxStore()を実行します。

その戻り値のストアのインスタンスを変数に格納し、state、getters、actionsで定義したプロパティにアクセスします。
戻り値のストアのインスタンスを格納した変数がストアとしての役割をします。


### useXxxxStore()

関数useXxxxStore()は、どこで何回呼び出されても、常に同じストアのインスタンスを返すgetter関数です。すべてのコンポーネントが同じストアのインスタンス(オブジェクト)にアクセスすることが保証されます。

コンソールからPiniaの詳細を確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2784589/1457fd0d-e635-36ab-e569-a1df4d6a275a.png)

### storeToRefs()

ストアはリアクティブなオブジェクトなので、分割代入したりスプレッド構文を使用するとリアクティブでななくなります。

そこでstoreToRefs()を使用することで、分割代入を行ってもリアクティビティを保持することができます。

```js:
import { storeToRefs } from "pinia"

const { recipe } = storeToRefs(store)

onMounted(async() => {
    const { data } = await allRecipes()
    recipes.value = data.data
    console.log(recipe.value)
});

```

# State

Stateはストアを構成する要素の中心部分で、データ本体を表します。

Setup Syntax構文の記述例です。
```js:stores/recipe.js
import { defineStore } from "pinia";

export const useRecipeStore = defineStore('recipeStore', {
    state: () => ({
        recipes: [{
            id: 1,
            title: "First recipe",
            time: 20,
            price: 500,
            filename: "20230531_cNj3iaOi7Gq8xUZ.jpg"
        }, {
            id: 2,
            title: "Second recipe",
            time: 50,
            price: 700,
            filename: "20230531_z5htyKAoOlLgx9r.jpg"
        },],
        recipe: {
            id: 1,
            title: "First recipe",
            time: 20,
            price: 500,
            filename: "20230531_cNj3iaOi7Gq8xUZ.jpg"
        },
    }),
    // getters: {

    // }
});
```
Setup Syntax構文では、ref()を使ってリアクティブなプロパティの初期値を定義し、公開したいプロパティを含むオブジェクトを返す関数を渡します。

# Getters

Gettersはストアのstateの値を基に算出した値を取得する算出プロパティです。ゲッターは第一引数にstateを受け取り、値を返す関数を定義します。

```js:store/recipe.js
export const useRecipeStore = defineStore('recipeStore', {
    state: () => ({
        recipes: [],
        recipe: {
            id: 1,
            title: "First recipe",
            time: 20,
            price: 500,
            filename: "20230531_cNj3iaOi7Gq8xUZ.jpg"
        },
    }),
    getters: {
        // レシピの合計数を計算するゲッター
        totalRecipes: (state) => state.recipes.length,

        // 特定の価格以下のレシピを返すゲッター
        recipesBelowPrice: (state) => (maxPrice) =>
            state.recipes.filter(recipe => recipe.price <= maxPrice),

        // 最初のレシピのタイトルを大文字に変換するゲッター
        firstRecipeTitleUpperCase: (state) => state.recipes.length > 0 ? state.recipes[0].title.toUpperCase() : '',

        // 特定のIDに対応するレシピを返すゲッター
        getRecipeById: (state) => (id) =>
            state.recipes.find(recipe => recipe.id === id)
    },
    actions: {
        async fetchAllRecipes () {
            const { data } = await allRecipes()
            this.recipes = data.data
        },
    },
});
```

### 他のゲッターを参照

他のゲッターを参照して使用する場合はthisを介して他のゲッターｂにアクセスできます。

### 別のストアのゲッターにアクセス

別のストアのgettersを使用す際は、ゲッターの定義内で別のストアをuseXxxxStore()で生成し、別のストアのゲッターを参照します。

# Actions

defineStore()のactionsプロパティで定義します。
アクションを定義する関数では引数を自由に設定して、任意の値を返すことができます。

アクションはロジックを書くためのに使います。

### 他のアクションを参照

他のアクションを参照して使用する場合はthis
を介して他のアクションにアクセスできます。

### 非同期処理

Promiseを返す限り、アクション内で任意のAPIの呼び出しやfetch()によるデータの取得、他のアクションを待機できます。

### ローディング状態の表示

非同期処理に時間がかかる場合、stateにローディング状態を定義して、データの取得中は「Loading post（ロード中）...」と表示することもできます。

### 別のストアのアクションのアクセス

別のストアのアクションをアクション内で直接使用できます。
アクションの定義内で、別のストアのインスタンスを生成して、アクションにアクセスできます。


### $onAction

ストアの$onAction()を使うと、アクションの結果を監視できます。コールバックを受け取り、コールバックの引数には様々なプロパティを持つオブジェクトが渡されます。分割代入を使用してそれぞれのプロパティを取得して使用します。

| プロパティ | |
| --- | --- |
|name|アクションの名前|
|store|ストアのインスタンス|
|args|アクションに渡されたパラメータの配列|
|after|アクション完了時に実行される関数(afterフック)|
|onError|アクションがエラーになった場合に実行される関数(onErrorフック)|

# 共通メソッド

## $patch

stateの変数を1つずつではなく、いくつかまとめて変更を適用することができます。
オブジェクトで変更する形式と関数で変更するパターンがあります。

### オブジェクト

```js:
const store = useRecipeStore()

store.$patch({
    recipe: {
        title: "First recipe updated using $patch",
        time: 20,
        price: 1000,
        filename: "20230605_UaHcCejSDypPvjN.jpg"
    }
})
  
```

### 関数

```js:
const store = useRecipeStore()

recipeStore.$patch((state) => {
    stste.recipes.push({ 
        title: "First recipe updated using $patch",
        time: 20,
        price: 1000,
        filename: "20230605_UaHcCejSDypPvjN.jpg"
    })
    state.hasChanged = true
})
        
```

## $subscribe
stateの変更を監視するメソッドです。

```js:
const store = useRecipeStore()

store.$subsribe((mutation, state) => {

// mutation.events : 変更された値の情報を持つ
// mutation.storeId : store作成の際に設定したユニークキー
// mutation.type : 'direct' | 'patch object' | 'patch function'

console.log(mutation, state);
});
```

## $onAction

actionsの実行を追跡します。

```js:
store.$onAction(
  ({
    name, // アクションの関数名
    store, // storeインスタンス
    args, // 引数
    after, // return or resolve後のフック
    onError, // エラー時のフック
  }) => {
    // Action実行前
    const startTime = Date.now()
    console.log(`Start "${name}" with params [${args.join(', ')}].`)

    // 実行完了後の処理
    after((result) => {
      console.log(
        `Finished "${name}" after ${
          Date.now() - startTime
        }ms.\nResult: ${result}.`
      )
    })

    // エラー時の処理
    onError((error) => {
      console.warn(
        `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
      )
    })
  }
)
```

$usbscribeと#onActionはデフォルトではunmounted時に追跡が解除されますが、それぞれの第二引数をtrueに変更すれば、unmounted移行もつ遺跡されます。