Vue3 チュートリアル 6 ダイナミックルーティングとPinia

https://qiita.com/TakahiRoyte/private/9033b61a0e528324da4c

前回はVue Rouerを導入しましたが、色々と問題がでてきました。

- ページ切り替えでインスタンス内のデータが消えてしまう
- 編集モードに移行しても文字列が表示されないので編集できない

これらの問題を解決するために以下のゴールを達成していきましょう。

### ゴール

- Piniaによるグローバルストアでデータを管理する
- Piniaの`state`, `actions`, `getters`のそれぞれを知り使う
- Vue Rouerのダイナミックルーティングを知り、使う

## Pinia

PiniaとはVueアプリケーション上でグローバルなストアを提供するライブラリです。
Vue2の時はVuexというストアのライブラリが利用されることが多かったですが、
その思想を受け継ぎながらもかなりシンプル化されて利用しやすさが上がったのがPiniaです。
Pinia自体はComposition APIでの利用を想定して作られましたが、OptionsAPIでも利用可能です。

### Piniaのインストール

前回のVue Routerをインストールしたのと同じ要領でPiniaをインストールします。
今回は最新バージョンで良いので、その場合はバージョン指定は不要です。

```bash
$ npm install pinia
```

そうするとVue Routerの時と同じく`package.json`に`pinia`が追加されているはずです。
続いては、ソースコード側にもPiniaを使う設定をいれていきます。修正するのはまたも`main.js`です。
`createPinia`をVueの`app.use`で登録します。

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

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

### Piniaのストアで`state`を書く

環境ができたら、実際にストアを定義していきましょう。
`src`ディレクトリの中に`stores`ディレクトリを作成し、`todos.js`ファイルを作成します。
そして`pinia`の`defneStore`を使って、まずは`state`（状態）として`todos`を定義しましょう。
`todos`はTodoオブジェクトの配列が入ります。

```js:todos.js
import { defineStore } from 'pinia'

export const useTodosStore = defineStore('todos', {
  state: () => {
    return {
      todos: [
        {
          isDone: false,
          text: 'Todo in Pinia Store!',
        },
      ],
    }
  },
})
```

ここで定義した`state`を読み込むよう`Home.vue`を修正していきます。
順番に。まずはストア内のデータを表示できるか試してみましょう。
`Home.vue`のようにコンポーネントで利用する際は定義したストアを呼び出します。
先程作った`todos.js`の`useTodosStore`を呼び、`pinia`から`mapStores`で利用します。

```vue:Home.vue
<script>
import TodoAdd from '@/components/components/TodoAdd.vue'
import TodoList from '@/components/components/TodoList.vue'
import { mapStores } from 'pinia'
import { useTodosStore } from '@/stores/todos'

export default {
  components: {
    TodoAdd,
    TodoList,
  },
  // ここは削除する
  // data() {
  //   return {
  //     todos: [
  //       //{ isDone: false, text: 'ToDoの文字列' }
  //     ],
  //   }
  // },
  computed: {
    ...mapStores(useTodosStore),
  },
  methods: {
    // 略
  },
}
</script>

<template>
  <TodoAdd @delete-done="clearDoneTodos" @add-todo="addTodo" />
  <p v-if="todosStore.todos.length === 0">ToDoがまだありません！</p>
  <TodoList v-else :todos="todosStore.todos" />
</template>

// 略
```

`data`に定義していたコンポーネントのデータは不要になったのでコメントアウト（削除）しています。
代わりに`computed`というプロパティが現れ、ここで`...mapStores(useTodosStore)`と実装しています。
この実装については順を追ってみていきます。

まず`computed`についてですが、これはcomputed(算出)プロパティと呼ばれます。
ストア専用のものではなく、元々はテンプレートの中に複雑なロジックを埋め込まないようにするためのものです。
例えば`data`に持っている日付の文字列`20220101`をテンプレート内で表示するに当たり、
`2022年01月01日`と複数の場で表示したい要望があるとします。
それを<code>{{ `${dateStr.substring(0,4)}年${dateStr.substring(4,6)}月${dateStr.substring(6,8)}日}` }}</code>と
毎回テンプレート内に記述するとなると、可読性が低いコードが大量にテンプレート内に実装されしまいます。
これを避けるために`computed`を使って、以下のように実装します。

```vue:computedの例
<script>
export default {
  data() {
    return { dateStr: '20220101' }
  },
  computed: {
    formattedDateStr() {
      return `${this.dateStr.substring(0,4)}年${this.dateStr.substring(4,6)}月${this.dateStr.substring(6,8)}日}`
    }
  }
}
</script>

<template>
  {{ formattedDateStr }} // 2022年01月01日
  {{ formattedDateStr }} // 2022年01月01日
  {{ formattedDateStr }} // 2022年01月01日
</template>
```

テンプレート内の可読性が悪いなと思ったらぜひ使ってみてください。

`computed`の値はもう1つ機能があって、それは`computed`のデータ参照元のデータが変更されると、
`computed`のデータも動的に算出されなおす、という動きがあります。
上の例の`dataStr`が変わると`computed`の`formattedDateStr`も変わるということです。
Piniaはこの機能を利用して、Piniaのストアのデータが変わると、
動的にコンポーネント内で利用する値も変わるわけです。この動きは後ほど確認します。
まずは、先程の実装を通して、`todos.js`に定義したTodoアイテムがブラウザで表示されてるか確認してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89911/3de8b3d3-0753-b476-ec3a-6c9b8bc7ac74.png)

この状態からEdit画面に一度いって、Home画面に戻ってきても同じ表示になっていると思います。
これはストアのTodosリスト情報を参照しているからです。ただ、現状ではリストを更新する手段が無いので実装していきます。

### Piniaで値を更新する`actions`を書く

Piniaの値を更新するメソッドは`actions`と呼ばれます。
まずは`todos`に新たなアイテムを追加する`addTodo`の`action`を書いてみましょう。
コンポーネントに実装していた`addTodo`とほとんど変わりはありません。

```js:todos.js
import { defineStore } from 'pinia'

export const useTodosStore = defineStore('todos', {
  state: () => {
    return {
      todos: [
        {
          isDone: false,
          text: 'Todo in Pinia Store!',
        },
      ],
    }
  },
  actions: {
    addTodo(todo) {
      this.todos.push(todo)
    },
  },
})
```

これをコンポーネント側から呼び出して利用します。
`mapStores`と同じように、今度は`mapActions`をインポートして使います。
ここで一つ注意することがあります。
ストアに書いた`addTodo`とコンポーネント側の`methods`の`addTodo`があります。
これらのどちらも同じ名前のため、`this.addTodo`としてアクセスしようとすると判別がつきません。
もとの`addTodo`は新規でTodoを追加するためのメソッドなので`addNewTodo`と名前を変えましょう。

```vue:Home.vue
<script>
import TodoAdd from '@/components/components/TodoAdd.vue'
import TodoList from '@/components/components/TodoList.vue'
import { mapActions, mapStores } from 'pinia'
import { useTodosStore } from '@/stores/todos'

export default {
  components: {
    TodoAdd,
    TodoList,
  },
  computed: {
    ...mapStores(useTodosStore),
  },
  methods: {
    ...mapActions(useTodosStore, ['addTodo']),
    addNewTodo(newTodoText) {
      if (!newTodoText) return alert('文字を入力してください')
      this.addTodo({
        isDone: false,
        text: newTodoText,
      })
    },
    clearDoneTodos() {
      this.todos = this.todos.filter((todo) => !todo.isDone)
    },
  },
}
</script>

<template>
  <TodoAdd @delete-done="clearDoneTodos" @add-todo="addNewTodo" />
  <p v-if="todosStore.todos.length === 0">ToDoがまだありません！</p>
  <TodoList v-else :todos="todosStore.todos" />
</template>
```

テンプレート内で呼ぶメソッド名を`addNewTodo`にするのもお忘れなく。
これで動作をするか確認してみましょう。

以前、Vue.js DevToolsを使ってコンポーネントの状態を確認したことがあったと思います。
同じようにPiniaもDevToolsによってストアの内容を確認できます。
実際に見てみましょう。

DevToolsのタブにPiniaというタブがあるのでクリックします。
するとストアごとに名前が表示されるのでそれを更にクリックすると、
今回は`state`が`todos`だけなのでそれの状態が確認できますね。
試しに新たなTodoを追加してストアが更新されるか試してみてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89911/e5d32cfe-ff2b-e4a2-7f9b-33f7a86d41ce.png)

追加はできるようですね。ただ、完了済みの削除は動かずコンソールにエラーが出てます。
そこで、次は同じ要領で、次は完了済みのTodoを消す処理の実装を試してみてください。
必要に応じて、以下のヒントを読んだり、その下にあるコードを参考にしてみましょう。
パターンAとBを用意したので、見る場合は順番に見ていってください。

__ヒント：パターンA__

1. `todos.js`の`actions`に削除する処理を書く
1. `Home.vue`のmethodsから1.で定義した処理を呼び出す
1. `actions`と`methods`のメソッド名がかぶってないかチェック（かぶってたら変える）
1. `mapActions`に1.で実装した処理を登録する
1. 必要に応じて`template`内で呼ばれている処理名も修正

おそらくエラーが出たらヒントのどこかでつまづいているはずです。
正解のコードの例を見てみましょう。

```js:todos.js
export const useTodosStore = defineStore('todos', {
  // 略
  actions: {
    addTodo(todo) {
      this.todos.push(todo)
    },
    deleteDoneTodos() {
      this.todos = this.todos.filter((todo) => !todo.isDone)
    },
  },
})
```

```vue:Home.vue
<script>
// 略
  methods: {
    ...mapActions(useTodosStore, ['addTodo', 'deleteDoneTodos']),
    addNewTodo(newTodoText) {
      if (!newTodoText) return alert('文字を入力してください')
      this.addTodo({
        isDone: false,
        text: newTodoText,
      })
    },
    clearDoneTodos() {
      this.deleteDoneTodos()
    },
  },
// 略
</script>
```

これでも動作しますが、よりスマートな方法としてパターンBがあります。

__ヒント：パターンB__

1. `todos.js`の`actions`に削除する処理を書く
1. `Home.vue`のmethodsから1.で定義した処理を呼び出す
1. テンプレート内から直接呼び出した処理を実行するようにする

ステップ数が少ないですね。具体的なコードになるとどうなるかを見てみましょう。

```js:todos.js
export const useTodosStore = defineStore('todos', {
  // 略
  actions: {
    addTodo(todo) {
      this.todos.push(todo)
    },
    clearDoneTodos() {
      this.todos = this.todos.filter((todo) => !todo.isDone)
    },
  },
})
```

さっきは`actions`のメソッド名をわざわざ変えていましたが、今回は`clearDoneTodos`ですね。
`Home.vue`の方も見てみましょう。

```vue:Home.vue
<script>
// 略
methods: {
    ...mapActions(useTodosStore, ['addTodo', 'clearDoneTodos']),
    addNewTodo(newTodoText) {
      if (!newTodoText) return alert('文字を入力してください')
      this.addTodo({
        isDone: false,
        text: newTodoText,
      })
    },
  },
//略
</script>

<template>
  <TodoAdd @delete-done="clearDoneTodos" @add-todo="addNewTodo" />
  <p v-if="todosStore.todos.length === 0">ToDoがまだありません！</p>
  <TodoList v-else :todos="todosStore.todos" />
</template>
```

違いにお気づきでしょうか。
今回は、入力チェックやその他の処理がないため、`template`内から`mapActions`で登録された、
`clearDoneTodos`を直接呼び出して完了済みのTodoを削除するようにしています。
パターンAもわかりやすいといえばわかりやすいですが、少し冗長でした。

### Piniaの`getters`を使う

PiniaのストアにはVueコンポーネントの`computed`にも似た`getters`を定義できます。
機能はあまり変わらず、`state`の値をもとに動的に算出される値を提供します。
今回のTodosでは「完了済みのTodoの数を表示する」という用途で使ってみましょう。
全体のTodoの数はシンプルにlengthで取得できますが完了済みは`isDone`が`true`のTodoのみに絞る必要があります。

----