Piniaの基本的なつかいかた

# Piniaとは  
[Pinia](https://pinia.vuejs.org/)はVuejs向けの状態管理ライブラリだ。コンポーネント間やページ間でグローバルに状態共有することができる。  
VuejsのComposition APIを意識して実装されているものの、OptionsAPIやVue2に適用することも可能だ。
Vuex5.xのRFCの要求事項をほとんど満たしていたということもあって、[Vuexに代わってVue公式の状態管理ライブラリに指名されている。](https://vuex.vuejs.org/)すなわち、比較的新しいライブラリながらも本番案件に利用できるだけの機能が十分に備わっているといえる。  
この記事では取り上げないが、サーバーサイドレンダリング（ViteやNuxt.js）にも対応している。
ちなみに、発音は「ピーニャ」だ（発音記号で書けば`/piːnjʌ/`）。

# この記事で解説すること
[Pinia公式ドキュメント](https://pinia.vuejs.org/introduction.html)に倣いつつ、以下の項目について解説する。
 - インストール方法
 - 簡単な利用例
 - Store、State、Getters、Actionsの詳しい使い方

# インストール
以下コマンドで導入可能。
```bash
yarn add pinia
```

```bash
npm install --dev pinia
```

# 試してみる
## PiniaをVueアプリインスタンスに適用
以下のように、Piniaインスタンスを`App.use()`の引数に入れればVueアプリでPiniaが使用可能になる。
```typescript:main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

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

## Storeの作成
まずは`defineStore()`を使用してStoreを作成する。  
`defineStore()`の第一引数にはStoreの一意なIDを付け、第二引数にStoreの定義を書く。  
そして`defineStore()`の戻り値をエクスポートし、他のモジュールで使うことになる。  

```typescript:myStore.ts
import { defineStore } from 'pinia'

export const useMyStore = defineStore('myStore', {
  //保持したいデータ
  state: () => {
    return {
      count: 0,
    }
  },
  getters: {
    double: (state) => {
      return state.count * 2
    },
  },
  actions: {
    increment() {
      this.count++
    },
  },
})
```
PiniaのStoreには
- state
- getters
- actions

の3つのプロパティを定義できる。
stateにはデータの読み書き、gettersには加工したデータの読み取り、actionsには各種メソッドを実装していく。  
これらはVuejsのdata, computed, methodsに相当するものと考えればわかりやすい。  
他にもwatchに相当するものとして、[Stateの変更](#stateの変更監視)や[Actionsの呼び出し](#actionsの呼び出しを監視する)の監視機能がある。

## コンポーネントからの利用
PiniaのStoreをコンポーネントで使う場合の例が以下になる。
記述量が減るので、`<script setup>`での利用を推奨。
```vue:MyComponent.vue
<template>
  <div class="hello">
    <button @click="myStore.increment">increment</button>
    <p>Count: {{ myStore.count }}</p>
    <p>DoubleCount: {{ myStore.double }}</p>
    <p>Count(String): {{ countString }}</p>
  </div>
</template>

<script lang="ts" setup>
  import { computed } from 'vue'
  import { useMyStore } from '@/myStore'
  const myStore = useMyStore()
  const countString = computed(() => {
    return String(myStore.count)
  })
</script>
```
`script setup`を利用しない場合は以下のようになる。
注意点として、`useMyStore()`は`setup()`関数内で実行しなくてはならない。
理由は後述の「[コンポーネント外でStoreを使う](#コンポーネント外でstoreを使う)」を参照のこと。

```vue:MyComponent.vue
<template>
  <div class="hello">
    <button @click="myStore.increment">increment</button>
    <p>Count: {{ myStore.count }}</p>
    <p>DoubleCount: {{ myStore.double }}</p>
    <p>Count(String): {{ countString }}</p>
  </div>
</template>

<script lang="ts">
  import { computed, defineComponent } from 'vue'
  import { useMyStore } from '@/myStore'

  export default defineComponent({
    setup() {
      const myStore = useMyStore()
      const countString = computed(() => {
        return String(myStore.count)
      })
      return {
        computed,
        myStore,
        countString,
      }
    },
  })
</script>
```

コンポーネントからStoreを利用するには、まず先ほどエクスポートした`useXXX`をインポートし、関数として実行した戻り値をStoreとして扱う。
```typescript
import { useMyStore } from '@/myStore'
const myStore = useMyStore()
```

あとはこのStoreに登録された各stateなどを呼び出せばよい。  
```typescript:SomeComponent.vue
// stateの読み取り
const count = myStore.count

// stateの更新
myStore.count = 5

// gettersにアクセス
const doubledCount = myStore.double

// actionsを実行
myStore.increment()
```
当然ながら、違うファイル内でも同じStoreを呼び出せば値は共有される。
```typescript:AnotherComponent.vue
import { useMyStore } from '@/myStore'
const myStore = useMyStore()
console.log(myStore.count) // 5
```
Vuexと比べると、`state`の読み取りに`.state`を挟む必要がない、`store.commit()`無しで状態更新ができるなど、構文が短くなっている。
以上のように、かなりスッキリとした構文でStoreを利用することができる。

なお、非同期処理関数を使いたい場合には後述のように[Actionsで実装する](#非同期処理を行うactions)ことになる。Gettersには非同期処理関数を登録できないので注意。

# より詳しい使い方
## Store
### 命名規則
`defineStore`の戻り値の関数は[Composableな関数](https://vuejs.org/guide/reusability/composables.html)にあたる。  
CompositionAPIの慣習に則り、Pinia公式では戻り値の名前を`useXXX`の形式にすることを推奨している。
（参考：https://pinia.vuejs.org/core-concepts/）

### コンポーネント外でStoreを使う
PiniaのStoreはPiniaインスタンスに依存している。
そのため、Piniaインスタンスが準備されていない環境で`useMyStore`などを呼び出すとエラーになる。
```typescript
useMyStore()
// getActivePinia was called with no active Pinia. Did you forget to install pinia?
//   const pinia = createPinia()
//   app.use(pinia)
```

Storeのテストを行うなど、Component外でStoreを利用したい場合は、`useMyStore()`にPiniaインスタンスを注入すればよい。
方法としてはPiniaインスタンスを作って注入する方法と、`setActivePinia()`を使う方法の2通りがある。
```typescript
import { setActivePinia, createPinia } from 'pinia'
import { useMyStore } from './myStore'

// 方法1：Piniaインスタンスを作って普通に注入
const pinia = createPinia()
const myStore = useMyStore(pinia)

// 方法2：setActivePinia()を利用
setActivePinia(createPinia())
const myStore = useMyStore()
```

### 複数のStore
Piniaで複数のStoreを使いたい場合は、それぞれを別のファイルに定義する。
Store間で連携が必要になった場合でも、`useXXX()`でStoreを呼び出せばそのstateなどを利用できる。
とはいえ、2つのStoreが相互に参照しあうようであれば1つに統合した方がよい。  

以下は、`myStore`の他に`anotherStore`を作成し、その中で`myStore`のstateを呼び出す例。

```typescript:anotherStore.ts
import { defineStore } from 'pinia'
import { useMyStore } from './myStore'

export const useAnotherStore = defineStore('anotherStore', {
  state: () => {
    return {
      text: '',
    }
  },
  getters: {
    textPlusCount: (state) => {
      const myStore = useMyStore()
      // myStoreのstate呼び出し
      return state.text + myStore.count

      // getters呼び出し
      // myStore.double

      // actions呼び出し
      // myStore.increment()
    },
  },
})
```

### リアクティブ性を保ちつつスプレッド構文を使う
PiniaのStoreは`reactive`でラップされたオブジェクトなので、スプレッド構文で展開すると、StateやGettersのリアクティブ性を壊してしまう。

```typescript
const myStore = useMyStore()
// ❌ リアクティブ性が壊れてしまう　＝　変更を検知できない
const { name, doubleCount } = myStore
```

リアクティブ性を保つには、`storeToRefs()`を用いるとよい。  
なお、actionsはそのままスプレッド構文を用いても問題ない。
```typescript
import { storeToRefs } from 'pinia'

const store = useMyStore()
// リアクティブな参照になる
const { name, doubleCount } = storeToRefs(myStore)

// actionsはそのままスプレッド構文を用いてもOK
const { increment } = myStore
})
```

## State
### Stateへのアクセス
Storeに定義したStateは、基本的に直接アクセス可能。例えば、下記のように加算演算子を使っても問題なく動作する。
```typescript
myStore.counter++
```

### Stateのリセット
`store.$reset()`を呼び出すことで、Stateを定義した初期状態にリセットすることができる。

```typescript
myStore.count = 100

// ...

// countの値が、Store定義時にStateとして設定した値に戻る。
// この場合は0になる。
myStore.$reset()
```

### Stateの変更
Stateを変更する際、Piniaでは`store.counter = 1`や`store.counter++`のように直接操作できることを既に学んだ。  
この他に、`store.$patch`メソッドを呼んで変更する方法をとることもできる。  
このメソッドは、変更後のStateを直接引数に入れるのと、stateを引数にとるコールバックを引数に入れる使い方の2通りが用意されている。  
基本的には前者でよいが、配列の操作など変更内容が複雑な場合には後者を使うとよい。
```typescript
// オブジェクトを代入
myStore.$patch({
  count: myStore.count + 100
})

// コールバック
myStore.$patch((state) => {
  state.count = state.count + 100
})
```

### State全体の置き換え
あるStoreのState全体を置き換える操作は、`store.$state`に直接代入することで実行できる。
```typescript
myStore.$state = {
  count: 24,
  isCountStop: true,
}
```

さらに、Piniaインスタンスが持つすべてのStateを置き換えるには次のようにすればよい。  
（サーバーサイドレンダリングの[State hydration](https://pinia.vuejs.org/ssr/#state-hydration)に使うらしい）
```typescript
pinia.state.value = {}
```

### Stateの変更監視
`$subscribe()`メソッドを使うことで、Stateに変更があった場合に行いたい処理を設定できる。引数には`mutation`と`state`を引数にとるコールバックを登録する。
 - `mutation.type`は、前述のStateの変更時に使った方法によって異なる値になる。直接代入なら`'direct'`、`$patch()`の引数にオブジェクトを入れれば`'patch object'`、関数を入れれば`'patch function'`になる。  
 - `mutation.storeId`は、Storeの定義時に設定したStoreのIDを格納している。  
 - `mutation.payload`は`mutation.type`が`'patch object'`の時にのみ、`$patch()`の引数に代入したオブジェクトを格納している。
```typescript
// mutation.typeの型
// import { MutationType } from 'pinia'
myStore.$subscribe((mutation, state) => {
  console.log(mutation.type) // 'direct' | 'patch object' | 'patch function'
  // myStore.$idと同じもの
  console.log((mutation.storeId) // 'myStore'
  // mutation.type === 'patch object'の時のみ有効
  console.log(mutation.payload) // myStore.$patch()に渡したオブジェクト

  // Stateに変更があったときに実行したい処理を書いていく
  // ...
})
```


## Getters
### 同じStore内の他のGettersにアクセス
同じStore内の他のGettersの戻り値を参照したい場合は、`this`を使ってアクセスする。  
その場合
 - アロー関数は使うことができない(`this`が`undefined`になるため）
 - `state`にアクセスしない場合、引数の`state`も取り除く（linter設定しだいではエラーになる）

に気を付けたい。
```typescript
export const useMyStore = defineStore('myStore', {
  state: () => {
    return {
      count: 0,
    }
  },
  getters: {
    double: (state) => {
      return state.count * 2
    },
    // アロー関数はthisがundefinedになる可能性があるため使えない
    doublePlusOne(): number {
      // thisでアクセスする
      return this.double + 1
    },
  },
})

```
### Gettersに引数を渡す
Gettersに引数を渡したい場合、Gettersの戻り値を値ではなく関数にする。  
その関数の引数には、Gettersに渡したい引数を設定する。
```typescript
export const useMyStore = defineStore('myStore', {
  state: () => {
    return {
      count: 0,
    }
  },
  getters: {
    double: (state) => {
      return state.count * 2
    },
    multiplied: (state) => {
      // 関数を返すようにする
      return (num: number) => state.count * num
    }
  },
})
```

呼び出し側では通常の関数のように利用できる。
```typescript
// 呼び出し
myStore.multiplied(5)
```

## Actions
### 非同期処理を行うActions
PiniaのActionsには、非同期処理を実行する関数も登録できる。  
通常の非同期処理関数のように`async`キーワードを使えばよい。
```typescript
export const useMyStore = defineStore('myStore', {
  state: () => {
    return {
      count: 0,
    }
  },
  actions: {
    async fetchCount(){
      const fetchedCount = await fetchXxxValue()
      this.count = fetchedCount 
    }
  },
})
```

### Actionsの呼び出しを監視する
`$onAction`メソッドを使うと、PiniaのストアのActionsが実行されるタイミングで別の処理を実行させることができる。
`$onAction`の引数に実行したい処理を行うコールバックを代入する。
処理を実行させたいタイミング（action実行前、実行後、例外発生時）に応じて、次の3か所に処理を実装していくことになる。下記のサンプルコードも参照のこと。
 - action実行前：　`$onAction`に渡すコールバック内
 - action実行後：　`after`に渡すコールバック内
 - 例外発生時：　`onError`に渡すコールバック内

```typescript
myStore.$onAction(
  ({
    name, // 実行されたactionの名前
    store, // Storeのインスタンス。`myStore`に同じ
    args, // actionに渡された引数の配列
    after, // actionの完了後（Promiseであれば`resolve()`された後）に実行する処理
    onError, // actionで例外が投げられたとき（Promiseであれば`reject()`されたとき）に実行する処理
  }) => {
    // action実行前に行われる処理
    console.log({
      name,
      store,
      args,
    })
    
    after((result) => {
      // action実行後に行われる処理
      console.log(result)
    })

    onError((error) => {
      // actionでエラーが発生したときに行われる処理
      console.error(error)
    })
  }
)
```

なお、この機能ではStoreのActions実行を監視できるが、__コールバックはStore内のすべてのActionsに対して実行される。すなわち、個別のactionのみを直接監視することはできない。__  
その場合は以下のようにaction実行前に行う処理の中でactionの名前（`name`）をチェックし、後続処理を続けるか分岐させるとよい。
```typescript
// action実行前に行われる処理
// `someAction`以外は無視
if(name !== 'someAction') return
```


さらなる機能として、上記で登録したコールバックの実行を止めさせることもできる。
そのためには`$onAction`の戻り値を受け取っておき、止めさせたいタイミングで実行する。
```typescript
const unsubscribe = myStore.$onAction(/* 略 */)

// 上で登録したコールバックを登録解除する
unsubscribe()
```

# まとめ
Vuejsの新たな状態管理ライブラリであるPiniaについて、以下の点を紹介した。
 - インストール方法
 - 簡単な利用例
 - Store、State、Getters、Actionsの詳しい使い方

この記事を参考にしながら、ぜひPiniaを使ってみてほしい。


# 参考
Pinia公式ガイド　https://pinia.vuejs.org/introduction.html  
公式ガイドにはこの記事では紹介しなかったトピック（サーバーサイドレンダリング対応、Vuex≦4からの移行など）も紹介されているため、必要になった場合は参照してほしい。