メモアプリを Vuex で管理する
index.vue ファイルとコンポーネントだけで作れる簡単なアプリですが、 Vuex による状態管理まで理解したかったので今回 Vuex を取り入れることにしました。
小さいアプリケーションだとデータの流れが分かりやすいので、これから Vuex を学びたい人におすすめです。
前提条件
-
Nuxt.js をインストール済 バージョン 6.4.1 (
npm -v
で確認) -
Nuxt.js 公式ドキュメントのチュートリアルを参考に、新しいプロジェクト名で 「create-nuxt-app」 の設定をします。作業はターミナル上で、ルートで行います(新しいプロジェクト名のディレクトリがルートに出来上がります)。これで簡単に開発の準備が整います。
-
何もない状態(スクラッチ)から始める場合も上記ドキュメントの中に、続けて記載がありますのでそちら参考にしてみてください。
ディレクトリができ、エディタを開くと↓写真のようなフォルダ一覧があります。http://llocalhost:3000で確認すると、 pages フォルダの中の index.vue の内容になっています。 h1 タグのプロジェクト名の下に自動で description の文字が入っている場合があるので、変更するときは package.json を編集します。(写真の下から3番目)
Flux の考え方 → Vuex を利用する場面
Flux についての説明は省きますが、 Vuex とは、ドキュメントによると
Vuex は Vue.js アプリケーションのための 状態管理パターン + ライブラリです。
Vuex は一箇所にアプリケーションの状態(ステート)がまとまっています。つまり、"Single source of truth"を保てます。情報源を信頼できるものにしようという事で、下記のように流れは一方向です。
State を変更できるのは Mutations のみであり、 Actions は「コミット」しかできません。
必要なファイルを揃える
さっそく作っていきますが、必要なファイルを3つ用意します。ファイルは template タグでくるみます。私のエディタは Emmet が入っておりscaffold
と入力するとテンプレートが用意されます。
- pages/index.vue (最初に既にあるはずです)
- components/MemoList.vue (文字はパスカルケースで作成/ 再利用可能な部品のようなもので Model 。)
- store/memo.js ( state, mutations, actionsを今後記述します)
フォームを作る
input タグを作るときは、v-model
で Model, View の双方向バインディングを可能にします。片方変更したら、( input の中身について DOM やデータに変更があれば)もう片方も同じように変更されます。
@click
とはv-on
のイベントハンドラの省略。
props でコンポーネントに値を渡す際は、ケバブケースmemo-contents
で渡します。コンポーネント側で受け取る時の属性はキャメルケース。
<div class="input_container">
<p>
<input
v-model="inputText"
type="text"
>
</p>
<button @click="addMemo()">Save</button>
</div>
<!-- コンポーネントに分けた部分について記述-->
<!-- memo-contents が属性で、" memoContents "はここでは変数です-->
<!-- @delete は、ただのきっかけで memo.js アクションの deleteMemo メソッドに伝えているだけ-->
<memo-list
:memo-contents="memoContents"
@delete="deleteMemo()"
/>
コンポーネントを作る
input 入力したものは一つ一つがメモリストです。何度も使うのでコンポーネントとして MemoList.vue ファイルに記述します。
ここでul
タグの中でv-for
を用いてリストを複数生成しています。
先にscript
タグのprops:
を見てください。memoContents
があり、ここで配列を作っています。
先程のリストの複数生成時にはここのことを指しており、 memoContents に入っている分だけインデックスを付けてアイテム( リスト )を生成します。
<template>
<div>
<ul
v-for="(item, index) in memoContents"
:key="index"
>
<li>
<span>{{ item.text }}</span> <!-- ここについては後ほど -->
<span>{{ item.date | dateConvert}}</span> <!-- ここについては後ほど -->
<button @click="handleDelete(index)">x</button>
</li>
</ul>
</div>
</template>
<script>
export default { //DOMに描き出すためにこのように書きます
props: { //親である index.vue から受け取るときはこの中に入ります
memoContents: { // memoContents として受け取り、値を一つずつ配列[]に入れます
type: Array,
default: () => []
}
},
index.vue に追記
コンポーネントを組み込む際には import する必要があります。
まだ Vuex に関する memo.js は記述していませんが、ここで呼び出します。methods
の中addMemo()
については連想配列の階層がコンポーネントで書いたものと一致する必要があります。
computed
では後述の memo.js の state の memoContents を参照。若干ややこしいですが、後述の memo.js と照らし合わせてみてください。
<script>
// MemoList.vue を import したら、 exportdefault の中に compornent として記述
import MemoList from '~/components/MemoList'
import { mapState, mapActions } from 'vuex' // このように記述する
export default {
components: { //ここでも描き出すためにMemoListを書きます
MemoList
},
data: () => ({
inputText: ""
}),
// 算出プロパティといいますが、 DOM へ表示されるものが computed です
computed: {
...mapState({
// どう index.vue で呼ぶか宣言して、 state の中の memo.js の中の memoContents を参照
memoContents: state => state.memo.memoContents
}),
// ...mapState('memo', ['memoContents'])とも書けます
},
methods: {
...mapActions('memo', ['addMemoContents','deleteMemoContents']),
addMemo() {
// methods 内の関数として呼び出し
this.addMemoContents({ //ここがコンポーネント側で{{ item.text }}で書いたところと階層が一致するはず
text: this.inputText,
date: new Date()
})
this.inputText = ""
},
// memoContens を消すものなので deleteMemo メソッドは index.vue にあるべき
// 子( MemoList.vue からの)の index を受け取っている
deleteMemo(index) {
this.deleteMemoContents(index)
}
}
}
</script>
MemoList.vue に追記
ここで重要なものがもうひとつ、子から親へ値を渡す$emit
です。
<script>
export default {
// component 内で完結するもの
filters: {
dateConvert: function (value) {
const date = new Date(value)
return `${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`
}
},
// 親のメソッドを発火させるためだけのもの
methods: {
//上記のクリックイベントの index を受け取って $emit で親に受け渡す
handleDelete(index) {
this.$emit('delete', index)
}
}
memo.js に記述
ようやくここまで来ました。今回重要なポイントは、「 mutations 以外は直接 state とやり取りしない」ことです。mutations が唯一 state を更新できるという事を踏まえて見ていきます。
// state memoContentsが入っている状態
export const state = () => ({
memoContents: []
})
//mutations データを更新できる
export const mutations = {
//引数に state と書く。 memo は actions から渡される
updateMemoContents(state, memo) {
state.memoContents.push(memo)
},
deleteMemoItem(state, index) {
state.memoContents.splice(index,1)
}
}
// actions ・・ mutations を発動する
export const actions = {
// { commit }と書くことで mutations に渡せる
// index.vue からの memoDate を受け取ってから mutations に commit
addMemoContents({ commit }, memoData) {
commit('updateMemoContents', memoData)
},
deleteMemoContents({ commit }, index) {
commit('deleteMemoItem', index)
}
}
まとめ
今回初めて Vuex を勉強しましたが、まだまだ理解が足りない部分が多く、冗長になってしまいました。 Vuex を使うべきアプリではありませんが、小さいアプリで全体像が捉えられたのは良かったです。