前回はVue Rouerを導入しましたが、色々と問題がでてきました。
- ページ切り替えでインスタンス内のデータが消えてしまう
- 編集モードに移行しても文字列が表示されないので編集できない
これらの問題を解決するために以下のゴールを達成していきましょう。
ゴール
- Piniaによるグローバルストアでデータを管理する
- Piniaの
state
,actions
,getters
のそれぞれを知り使う - Vue Rouerのダイナミックルーティングを知り、使う
Pinia
PiniaとはVueアプリケーション上でグローバルなストアを提供するライブラリです。
Vue2の時はVuexというストアのライブラリが利用されることが多かったですが、
その思想を受け継ぎながらもかなりシンプル化されて利用しやすさが上がったのがPiniaです。
Pinia自体はComposition APIでの利用を想定して作られましたが、OptionsAPIでも利用可能です。
Piniaのインストール
前回のVue Routerをインストールしたのと同じ要領でPiniaをインストールします。
今回は最新バージョンで良いので、その場合はバージョン指定は不要です。
$ npm install pinia
そうするとVue Routerの時と同じくpackage.json
にpinia
が追加されているはずです。
続いては、ソースコード側にもPiniaを使う設定をいれていきます。修正するのはまたもmain.js
です。
createPinia
をVueのapp.use
で登録します。
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オブジェクトの配列が入ります。
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
で利用します。
<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日
と複数の場で表示したい要望があるとします。
それを{{
と${dateStr.substring(0,4)}年${dateStr.substring(4,6)}月${dateStr.substring(6,8)}日}
}}
毎回テンプレート内に記述するとなると、可読性が低いコードが大量にテンプレート内に実装されしまいます。
これを避けるために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アイテムがブラウザで表示されてるか確認してください。
この状態からEdit画面に一度いって、Home画面に戻ってきても同じ表示になっていると思います。
これはストアのTodosリスト情報を参照しているからです。ただ、現状ではリストを更新する手段が無いので実装していきます。
Piniaで値を更新するactions
を書く
Piniaの値を更新するメソッドはactions
と呼ばれます。
まずはtodos
に新たなアイテムを追加するaddTodo
のaction
を書いてみましょう。
コンポーネントに実装していたaddTodo
とほとんど変わりはありません。
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
と名前を変えましょう。
<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を追加してストアが更新されるか試してみてください。
追加はできるようですね。ただ、完了済みの削除は動かずコンソールにエラーが出てます。
そこで、次は同じ要領で、次は完了済みのTodoを消す処理の実装を試してみてください。
必要に応じて、以下のヒントを読んだり、その下にあるコードを参考にしてみましょう。
パターンAとBを用意したので、見る場合は順番に見ていってください。
ヒント:パターンA
-
todos.js
のactions
に削除する処理を書く -
Home.vue
のmethodsから1.で定義した処理を呼び出す -
actions
とmethods
のメソッド名がかぶってないかチェック(かぶってたら変える) -
mapActions
に1.で実装した処理を登録する - 必要に応じて
template
内で呼ばれている処理名も修正
おそらくエラーが出たらヒントのどこかでつまづいているはずです。
正解のコードの例を見てみましょう。
export const useTodosStore = defineStore('todos', {
// 略
actions: {
addTodo(todo) {
this.todos.push(todo)
},
deleteDoneTodos() {
this.todos = this.todos.filter((todo) => !todo.isDone)
},
},
})
<script>
// 略
methods: {
...mapActions(useTodosStore, ['addTodo', 'deleteDoneTodos']),
addNewTodo(newTodoText) {
if (!newTodoText) return alert('文字を入力してください')
this.addTodo({
isDone: false,
text: newTodoText,
})
},
clearDoneTodos() {
this.deleteDoneTodos()
},
},
// 略
</script>
これでも動作しますが、よりスマートな方法としてパターンBがあります。
ヒント:パターンB
-
todos.js
のactions
に削除する処理を書く -
Home.vue
のmethodsから1.で定義した処理を呼び出す - テンプレート内から直接呼び出した処理を実行するようにする
ステップ数が少ないですね。具体的なコードになるとどうなるかを見てみましょう。
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
の方も見てみましょう。
<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のみに絞る必要があります。