はじめに
本記事ではVue.jsの初心者向けとして記事を書きます。Vue.jsはフロントエンドのフレームワークを初めて扱う方でもとっつき易く、小さく始めることができる、また既存のJQueryを用いたプロジェクトから移行しやすいといった特徴があります。
今回はVue.jsを使う上でセットで使われることの多いVuexに関してできるだけ分かりやすく説明していく。他にも、SPAとして利用するvue-routerも使用機会が多いが本記事では割愛する。軽くではあるが実践的に役立つ知識を交えつつ文法を中心に説明する。
Vuexとは何か
Vue.jsを勉強し始めた方はコンポーネントの概念は理解されているかと思うがこのような疑問も浮かんできたのではないだろうか?
それぞれ独立しているコンポーネント間のデータを共有するにはどうすればいいのだろうか?
例えをだそう。
Header.vue, Main.vueの2つのコンポーネントがあったとする。ここにはそれぞれユーザ名を表示する箇所がある。
Vue.jsを使ったことがあるなら
export default {
data () {
return {
name: 'Tanaka'
}
}
}
といった構造を考えるのは容易いだろう。
だがこれには欠陥がある。何かというと、コンポーネント間で同じ名前を共有したいと思ったときにそれを実現することができないという点である(実際は工夫すればできるのだが本筋ではないので考えないものとする)。
なぜ、共有することができないのか?
コンポーネントが呼び出されるときに使われるdata()
は、それぞれのコンポーネントとして独立しているからである。
これはVue.jsのコンポーネントの再利用の項目を読んで頂ければ理解しやすいと思う。
オブジェクト指向的にいえば、たとえ同じクラスから作られたインスタンスであっても別物であるのと同じである(余談ではあるが、この性質を利用して再帰的にコンポーネントを利用する場面がある)。
ではどのようにして異なるコンポーネント間で同じデータを持たせれば良いのだろうか?
この疑問に答えてくれるのがVuexだ。
環境
続いて環境についてサラッとではあるが説明する。
Vuexは主にstore/index.jsファイルの中に記述する。そのため、本記事ではこのファイルの中にコーディングしていくことを想定していると仮定する(vue-cliで作られた場合は、3.0より直接store.jsという名前で管理されるようになった。ファイルがむき出しなのは違和感があるので本記事では想定しないが別に初期状態でも問題はない)。
フォルダ構成としては以下を想定する。
root/
├ main.js
├ App.vue
├ store/
│ └ index.js
├ components/
│ └ Header.vue
│ └ Main.vue
また環境としてはVue-CLIを利用した開発を想定とするので
vue init webpack <プロジェクト名>
やvue create <プロジェクト名>
としているものとする(補足: 前者のコマンドはvue-cliが2.xの場合。後者は3.xの場合である。くどい様だが一応)。
Vue-CLIが3.xの場合は初期化時にVuexをいれることができるが、2.xの場合はnpm install vuex --save
またはyarn add vuex
としておく。
以下Vuexの文法について触れていく。
Vuexには大きく分けて
- ステート
- ゲッター
- ミューテーション
- アクション
- モジュール
の5つの要素からなっている。
本記事は全部で3回を予定しており
①ステートとゲッターについて
②ミューテーションとアクションについて
③モジュールとVuexならではの問題とその対策
であげていきたいと思っています。
今回はステートとゲッターについて説明します。
ステート
ステートは先ほどの例に出したdata()間の共有を考えるのに有効である。
先ほどの例を想定してコーディングしていくと
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
name: 'Tanaka'
}
export default new Vuex.Store({
state: state
})
まずはVuexを使えるようにVue.use()にいれる。
次に、コンポーネント間で共有してあげたいデータを宣言してあげよう。state
変数内にdata()
で利用するように定義する。これを各コンポーネントで使えるようnew Vuex.Store
としstateキーの値としていれてあげよう。これで下準備は完了である(ちなみに、いれるキーが値と同じ場合は :
を省略することができる。そのため、state: state
ではなくstate
だけで宣言できる。)
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App)
}).$mount('#app')
次にmain.jsを見ていく。
こちらでは先ほど宣言したindex.jsをimportするため、storeディレクトリを読み込んでいる。これをVueを宣言する中にいれて利用準備は完了である。
<template>
<header>
<h1>{{ getName }}</h1>
</header>
</template>
<script>
export default {
name: 'header-sample',
computed: {
getName () {
return this.$store.state.name
}
}
}
</script>
呼び出しの部分を見ていく。
stateを呼び出すにはcomputedの中でreturn this.$store.state
として最後に呼び出したいstate名を選べば良い。
<template>
<main>
<h2>{{ getName }}</h2>
</main>
</template>
<script>
export default {
name: 'main-sample',
computed: {
getName () {
return this.$store.state.name
}
}
}
</script>
main.vueは上とほとんど同じなため説明を省略します。
ではこの状態で全てをマウントするApp.vueにコーディングしていく。
<template>
<div id="app">
<Header/>
<Main/>
</div>
</template>
<script>
import Header from './components/Header.vue'
import Main from './components/Main.vue'
export default {
name: 'app',
components: {
Header,
main,
}
}
</script>
以上でstateの表示をみることができる。早速localhostを立ち上げ確認すると、ふたつのコンポーネントがしっかりとnameステートを呼び出していることが確認できると思う。
今度はnameをfistName, lastNameとして分割してみよう
const state = {
firstName: 'Kenji',
lastName: 'Tanaka'
}
次に呼び出す部分だが、先ほどのように
getFirstName () {
return this.$store.state.firstName
}
としても良いがこれをふたつも書くのは流石に面倒である。
これの解決策としてVuexはmap
を用意している。これはステート以外にも有効な方法である。
<template>
<header>
<h1>{{ firstName }}</h1>
<h2>{{ lastName }}</h2>
<header>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'header-sample',
computed: {
...mapState({
'fistName': 'firstName',
'lastName': 'lastName'
})
}
}
</script>
Headerを例にしているが、どちらのコンポーネントにも共通した修正とする(当たり前だがMain.vueのh2タグは分かりやすく他のタグを用いても問題はない)。これでmapStateを用いたステートの共有化は完了である。
余談
...mapState([
'firstName',
'lastName'
])
のように配列にしていれることもできる。ステートを別名にする必要がなければこちらでも良い(今回は分かりやすさ重視でオブジェクトにいれています)。
ゲッター
次にゲッターをみていく。
ゲッターはどんなものかというとstateの状態を調べるのに利用するものである。Vue.jsでの中身に例えるならばcomputed()に相当するものであり、また複数のコンポーネントのcomputed()内にて同じ処理をしているものがあった場合、ゲッターに役割を投げることができないか検討すべきである。
今回はfirstNameの文字列の長さを求めるものとする。
実装は以下になる。
const getters = {
firstNameLength: state => {
return state.firstName.length
}
}
export default new Vuex.Store({
state: state,
getters: getters
})
gettersの場合は:state => {}
のようにアロー関数式として書くのが通例となる(JavaScriptアロー関数についてはES2015で調べればでてきます。ほかにも覚えておくと役立つ知識が多いので是非一度目を通しておくことをおススメしておきます)。またステートの中身を呼び出すときは、state.キー名としてアクセスできます。
これをHeader.vueから呼び出します
<template>
<header>
<h1>{{ firstName }}</h1>
<p>firstNameの長さは{{ firstNameLength }}です</p>
<h2>{{ lastName }}</h2>
<header>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'header-sample',
computed: {
...mapState({
'fistName': 'firstName',
'lastName': 'lastName'
}),
firstNameLength () {
return this.$store.getters['firstNameLength']
}
}
}
</script>
ゲッターもステートと同じくcomputed()内で呼び出します。このとき['firstNameLength']
とブランケット記法で呼び出しましたが、.firstNameLength
としてドット演算子を利用しても問題ありません。
さて、次にlastNameの長さを取得しますが、またreturn以下を書くのは面倒なのでmapを利用します。
const getters = {
firstNameLength: state => {
return state.firstName.length
},
lastNameLength: state => {
return state.lastName.length
}
}
<template>
<header>
<h1>{{ firstName }}</h1>
<p>firstNameの長さは{{ firstNameLength }}です</p>
<h2>{{ lastName }}</h2>
<p>lastNameの長さは{{ lastNameLength }}です</p>
<header>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
export default {
name: 'header-sample',
computed: {
...mapState({
'fistName': 'firstName',
'lastName': 'lastName'
}),
...mapGetters({
'firstNameLength': 'firstNameLength'
'lastNameLength': 'lastNameLength'
})
}
}
</script>
ここら辺は特に問題ありませんね!
さてこれで終わりにしたいのですが、実はゲッターは他に2点ある特徴があります。
まず1つは、ゲッターは別のゲッターを利用することができる点です。つまり共通化したい処理だけをゲッターにして、それを個別に呼び出すことが可能です。
もうひとつは通常のcomputed()で使うfunctionとは違い、引数を取ることができる(これをペイロードと言う)という点です。厳密にはこの使われ方をした瞬間はmethods内で呼び出さなければならないのですが、ゲッターの特徴のひとつなのでこちらであげようと思いました。
では上記二点を生かした一例を書いていきたいと思います。
const getters = {
firstNameLength: state => {
return state.firstName.length
},
lastNameLength: state => {
return state.lastName.length
},
nameLength: (state) => (first, last) => {
return first.length + last.length
},
fullNameLength: (state, getters) => (first, last) => {
return getters.nameLength(first, last)
}
}
<template>
<header>
<h1>{{ firstName }}</h1>
<p>firstNameの長さは{{ firstNameLength }}です</p>
<h2>{{ lastName }}</h2>
<p>lastNameの長さは{{ lastNameLength }}です</p>
<h3>フルネームの長さは{{ fullNameLength(firstName, lastName) }}です</h3>
<header>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
export default {
name: 'header-sample',
computed: {
...mapState({
'fistName': 'firstName',
'lastName': 'lastName'
}),
...mapGetters({
'firstNameLength': 'firstNameLength'
'lastNameLength': 'lastNameLength'
})
},
methods: {
fullNameLength (first, last) {
return this.$store.getters.fullNameLength(first, last)
}
}
}
</script>
特に難しいところは無いと思いますが、ペイロードを取るときはアロー関数式を: (state) => (name) => {}
のようにしてください。また、ペイロードを取るときはmethods内で呼び出すことだけ注意してください。
まとめ
ステート
各コンポーネントのdata()間で共有したいデータを扱える。
ゲッター
ステートの状態を調べるのに使う(状態を変えたい場合は次回説明するミューテーションを使います!間違いやすいので注意!)。利用場面としては、配列の中から特定の値を持ってるものだけを探したりするのに使う機会も多いです。これはfilterメソッドを使ってやりくりする事になると思います。
そしてペイロードを取るときはmethods内で使うことを忘れないこと。
次回はミューテーションとアクションについて述べたいと思います!