Vueの状態管理ライブラリPiniaとはナンゾや

Vueを再度学び直そうとした際に公式状態管理ライブラリがVuexではなくpinia(ピーニャ)に変更されていました。
色々調べて見ると**2021年12月**にはVueの公式プロジェクトとなったのがpiniaでした。(約2年後に知るという失態...)
ということで！学んだことをまとめて、簡単なTodoアプリを作成していきたいです。

## piniaとは
piniaはVuexの状態管理ライブラリの後継になります。
公式プロジェクトなる以前からVue3のCompositionAPIベースで開発はされていたのが、前述した通り**2021年12月**に公式プロジェクトになりました。
公式プロジェクトになったため`create-vue`を叩いた時にインストール・初期設定ができます。
また、既存のVueプロジェクトには`npm install pinia`で追加可能です。(追加時は不要になるVuexをuninstallしてください)

https://pinia.vuejs.org

## Vuexとの違い
調べた感じ、多くの違いがあったが自分が気になったのは「TypeScriptのフルサポート」と「mutationsの廃止」、「Composition APIベースで書ける」です。

### TypeScriptのフルサポート
Vuex4以前まではTypeScriptのサポートが不完全で使い勝手が悪い印象がありました。
ですが、pinia(Vuex5含め)では完全なサポートがされたため型推論されやすく使いやすいと感じました。

### mutationの廃止
今までのVuexではstateの値更新をしたい場合はmutaionで行っていたが、piniaではmutaionが廃止されました。
mutationがあるのはデータ更新を限定することで**actionsではAPIなどの非同期処理**、**mutaionではactionから受け取った値の加工・stateの更新**という形で「責務の分離」が出来て自分的には好きな仕組みでした。

ただ、Vuexは学習コストが高いのは否めないです。(自分もVuexの流れを理解するまで約2ヶ月かかりました...)
学習コストが高いためVuexが理解しにくいというエンジニアは多くないはずです。

だからこそ、学習コストが下がったためにVueで状態管理が出来るエンジニアが増えてくれたらと思います。

### Composition APIベースで書ける
後述しますが、piniaでは二種類の書き方があり、その中でComposition APIベースで書ける`Setup Stores`はVue3以降のプロジェクトではComposition APIで書くためコードの統一化が出来るためチーム開発時やVue2以前の`Option API`の書き方を知らないエンジニアにも書きやすくなっており学習コストが下がったと感じています。

### piniaの書き方
piniaには`Option Stores`と`Setup Stores`、二種類の書き方があります。
#### Option Stores
Vue2まで使われていたOption APIベースで書く方法になります。
現在のVue開発では`Composition API`の方がメジャーになっているため`Option API`よりも`Setup Stores`でpiniaを書くのが良いかと思います。

```ts:Option Stores
import { defineStore } from 'pinia'

type State = {
  count: number
  name: string
}

export const useCounterStore = defineStore('counter', {
  state: (): State => ({ count: 0, name: 'Eduardo' }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    }
  }
})
```

#### Setup Stores
Vue3から導入されたComposition APIベースで書く方法になります。
現在は多くのエンジニアがComposition APIでVueを書いていると思うので`Setup Stores`でpiniaを書く方がチーム開発時などでコードの統一が出来て開発がしやすくなるかと思います。

```ts:Setup Stores
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

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

  return { count, doubleCount, increment }
})
```

チームによっては`Setup Stores`ではなく`Option Stores`を使うかと思います。

https://pinia.vuejs.org/core-concepts

## piniaを使った簡単なTodoアプリ作成

:::note info
既にpiniaの使い方を知っている方は読み飛ばして問題ないです。
簡単なTodoアプリを作成して理解を深める内容になっています。
:::

### Vueのプロジェクトを作成
下記コマンドでプロジェクトを作成
```zsh
npm create vue@latest
```
コマンドを叩いた後、複数の質問がされるので個々の作業しやすいプロジェクトにしてください。
自分は下記の形でプロジェクトを作成しています。
```zsh
? Project name: › pinia-study-to-todo-app
? Add TypeScript? › Yes
? Add JSX Support? › No
? Add Vue Router for Single Page Application development? › Yes
? Add Pinia for state management? › Yes
? Add Vitest for Unit Testing? › No
? Add an End-to-End Testing Solution? › No
? Add ESLint for code quality? › Yes
? Add Prettier for code formatting? › Yes
```

プロジェクトが作成されたら
```zsh
cd pinia-study-to-todo-app
npm install
```
を叩いて必要なライブラリをインストールしていきます。


### viteの設定
:::note info
自分が使いやすいように初期設定から少し変更を加えるだけのため不要な方は飛ばして問題ないです。
:::

<details>
<summary>vite.config.tsにサーバーオプションを追加</summary>

viteの初期設定だとportが指定されていないので指定・開発用サーバー起動時にプロジェクトが自動でブラウザで開かれるようにしていきます。

```ts:vite.config.ts
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dns from 'dns' // 追加

dns.setDefaultResultOrder('verbatim') // 追加

// https://vitejs.dev/config/
export default defineConfig({
  ...,

  // 追加
  server: {
    port: 3000,
    open: true
  }
})
```

</details>

### Todoアプリ作成
#### ファイル構成
ファイル構成は下記になります。
基本的な構成はプロジェクト作成初期のままになっています。
```zsh
.
├── Makefile
├── README.md
├── env.d.ts
├── index.html
├── package-lock.json
├── package.json
├── public
│   └── favicon.ico
├── src
│   ├── App.vue
│   ├── assets
│   │   ├── base.scss
│   │   ├── logo.svg
│   │   └── main.scss
│   ├── components
│   │   ├── HelloWorld.vue
│   │   └── TaskItem.vue
│   ├── main.ts
│   ├── router
│   │   └── index.ts
│   ├── stores
│   │   └── todo
│   │       ├── index.ts
│   │       └── type.ts
│   └── views
│       ├── CompletedTodosView.vue
│       ├── HomeView.vue
│       └── IncompleteTodosView.vue
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
```

#### storesフォルダ
今回メインになるフォルダになります。
状態管理したいものをフォルドごとで管理する形にしています。
```zsh
stores
└── todo
    ├── index.ts
    └── type.ts
```

* `index.ts`ではpiniaのコード
* `type.ts`では状態管理している値の型を管理

```ts:src/stores/todo/type.ts
export type TodoType = {
  id: number
  text: string
  isCompleted: boolean
}
```

書き方としては一般的な`Composition API`の書き方になっています。
`Composition API`と同じ書き方のため、すぐに状態管理出来るのはPiniaの強みかと思います。
```ts:src/stores/todo/index.ts
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import type { TodoType } from './type'

export const useTodoStore = defineStore('todo', () => {
  const todos = ref<TodoType[]>([])
  const idCount = ref(1)

  const addTodo = (text: string) => {
    todos.value.push({
      id: idCount.value++,
      text: text,
      isCompleted: false
    })
  }

  const todosFilteredCompleted = computed(() => {
    return todos.value.filter((todo) => {
      return todo.isCompleted
    })
  })
  const todosFilteredIncomplete = computed(() => {
    return todos.value.filter((todo) => {
      return !todo.isCompleted
    })
  })

  const searchTodoById = (id: number) => {
    const todo = todos.value.find((todo) => todo.id === id)
    if (todo === undefined) throw new Error('Not Found Todo')

    return todo
  }
  const toggleIsCompletedTodo = (id: number) => {
    const todo = searchTodoById(id)
    todo.isCompleted = !todo.isCompleted
  }

  const todoCount = computed(() => todos.value.length)
  const completedTodoCount = computed(() => todosFilteredCompleted.value.length)
  const incompleteTodoCount = computed(() => todosFilteredIncomplete.value.length)

  return {
    todos,
    addTodo,
    todosFilteredCompleted,
    todosFilteredIncomplete,
    toggleIsCompletedTodo,
    todoCount,
    incompleteTodoCount,
    completedTodoCount
  }
})
```

#### TaskItem.vueコンポーネント
`useTodoStore`から`toggleIsCompletedTodo`を呼び出す。
`toggleIsCompletedTodo`はtodoが持っている`isCompleted`を切り替える関数です。

```vue:src/components/TaskItem.vue
<script lang="ts" setup>
import { useTodoStore } from '@/stores/todo'

defineProps<{
  id: number
  text: string
  isCompleted: boolean
}>()

const todoStore = useTodoStore()
const { toggleIsCompletedTodo } = todoStore
</script>

<template>
  <div class="card" :class="'card' + (isCompleted ? '__completed' : '__incomplete')">
    <div class="card--id">id: {{ id }}</div>
    <div class="card__body">
      <div class="card__body--text">{{ text }}</div>
      <button
        @click="toggleIsCompletedTodo(id)"
        class="card__body--button"
        :class="'card__body--button' + (isCompleted ? '__completed' : '__incomplete')"
      >
        <div v-if="isCompleted">完了済</div>
        <div v-else>完了</div>
      </button>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.card {
  padding: 12px;
  border-radius: 12px;

  &__completed {
    border: 1px solid hsl(160, 100%, 37%);
  }

  &__incomplete {
    border: 1px solid #ababab;
  }

  &--id {
    font-size: 1rem;
  }

  &__body {
    display: flex;
    justify-content: start;
    align-items: center;
    column-gap: 1rem;

    &--text {
      font-size: 2rem;
      font-weight: bold;
    }

    &--button {
      padding: 4px 14px;
      cursor: pointer;
      border-radius: 6px;
      border: none;

      &__completed {
        background: hsl(160, 100%, 37%);
      }
      &__incomplete {
        background: #ababab;
      }
    }
  }
}
</style>
```

#### HomeView.vue

`HomeView.vue`では管理しているtodo全てを表示されるようにしています。
store上の値の変更を検知しつつ参照するためにはPiniaのユーティリティ関数の`storeToRefs`を使用して値を呼び出す。

```vue:src/view/HomeView.vue
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useTodoStore } from '@/stores/todo'
import TaskItem from '@/components/TaskItem.vue'

const todoStore = useTodoStore()
const { todos } = storeToRefs(todoStore)
</script>

<template>
  <div class="card__wrap">
    <TaskItem
      v-for="todo in todos"
      :key="todo.id"
      :id="todo.id"
      :text="todo.text"
      :is-completed="todo.isCompleted"
    />
  </div>
</template>

<style lang="scss" scope></style>
```

`IncompleteTodosView.vue`や`CompletedTodosView.vue`は`HomeView.vue`と同じコードで書いています。

---

簡単に書いたコードを説明していきましたが、実際に動かして確認をしたい方は下記のリポジトリからcloneしていただければと思います。

https://github.com/roll1226/pinia-study-to-todo-app

## 最後に
Piniaについて学んできましたが、VueはReactとは違い誰でも扱いやすいものだなと感じました。
今回はVueを学び直そうとした際にPiniaというものを知りましたが、公式プロジェクトになったのは約2年前であることから、情報のキャッチアップをしっかり出来ていなかったと痛感しました。
今回の経験を忘れずにVue以外の言語もキャッチアップして可能であれば記事を投稿して自分の成長に繋げていければと思います。