【Nuxt 2】Vuex(Store)
Vuex とは
Vuex は、Vue.js アプリケーションにおいて「状態」を管理するのためのライブラリです。 この「状態」は、コンポーネントとは別で管理され、すべてのコンポーネントから参照・更新が可能です。
例えば SPA を構築する場合に、すべてのページでユーザー情報が必要だとします。
基本的にページ毎に状態(データ)を持つため、ページ遷移の度に状態はリセットされます。 つまりページ遷移の度に、何かしらの手段を用いてユーザー情報を取得する必要があります。
これを Vuex によって管理するようにします。 前述したように、Vuex はコンポーネントとは別で管理されます。 つまり、ページを遷移したとしても Vuex で管理している状態はそのまま保持されているため、ユーザー情報の取得は必要最低限で済みます。
Vuex の状態はメモリ上で管理されるため、ウィンドウを閉じたり、ページリロードをすると初期化される点に注意が必要です。
Vuex の基本
Vuex では、以下の図のように状態管理が行われます。 以下よりそれぞれの役割について説明します。
State
State は、そのまま「状態」を表します。 先程の例でいうユーザー情報など、すべてのコンポーネントで共通的に管理したいデータを設定します。
コンポーネントは、State を参照することができますが、直接更新することはできません。 更新する場合は、以下で説明する Mutations または Actions を使用します。
Mutations
Mutations は、State を更新するためのものです。 言い換えると、State を更新できるのは Mutations のみです。
Mutations は、Commit という形で呼び出され、State を更新する処理を行います。 上図では Actions からのみ呼び出されていますが、コンポーネントから直接呼び出すこともあります。
注意点として、Mutations は Commit 時に渡されるデータを基に State を更新するだけであり、 Mutations が自ら更新用のデータを取得することは行いません。 データを取得する役割を担うのは、Actions またはコンポーネントです。
Actions
上述したように、Actions は API などを介して外部からデータを取得するためのものです。 取得したデータを Mutations に渡すことで State を更新します。 Actions が State を直接更新することはできません。
Actions は、Dispatch という形でコンポーネントから呼び出されます。
コンポーネント自身が外部からデータを取得し、Mutations を呼び出しても良いのですが、 処理の共通化という観点で見たときに、Actions を利用したほうが良いケースは多いはずです。 これについてはケースバイケースで判断してください。
Getters
上図には記載がありませんが、Getters というものがあります。 これは、State を加工した状態で参照する場合に使用します。 例えば State で管理している日付を「yyyy/MM/dd」にフォーマットされた状態で参照したい、というように使用します。
Vuex の利用
定義
NuxtJS にはデフォルトで Vuex が搭載されています。
利用するためには、store
フォルダを作成し、index.js
ファイルを作成します。
以下はユーザー情報を管理するための簡単な例になります。
import axios from 'axios'
export const state = () => ({
user: null,
})
export const getters = () => {
userName: state => {
return state.user?.name ?? 'No Name';
}
}
export const mutations = {
set(state, user) {
state.user = user
},
clear(state) {
state.user = null
},
}
export const actions = {
async fetch(context, id) {
const { data } = await axios.get(`/api/user/${id}`)
context.commit('set', data)
},
async update(context, user) {
await axios.put(`/api/user/${user.id}`, user)
context.commit('set', user)
}
}
以下は、定義に関する補足事項です。
Mutations、Actions へは、データは1つにまとめて渡す
Mutations と Actions で定義するメソッドでは、引数が決まっています。
第一引数は、各々で決まったオブジェクト(Mutaions はstate
、Actions はcontext
)となります。
この設定は、必然的に必須となります。
第二引数は、処理に必要なすべてのデータとなります。
つまり、Mutations や Actions を呼び出す際は、必要なデータをすべて 1 つのオブジェクト(または値)として渡す必要があります。
ちなみに第三引数以降は無視されます(undefined
になります)。
具体的に、id
とname
をデータとして渡したい場合は、{ id, name }
のようにまとめる必要があるということです。
当然メソッドは、user.id
、user.name
のように参照する必要があります。
Actions の context は、Vuex の情報
Actions の第二引数であるcontext
は、以下 Vuex の情報を持ちます。
state
:自身の Stategetters
:自身の Getterscommit
: Mutations の実行dispatch
:Actions の実行rootState
:すべての StaterootGetters
:すべての Getters
今回は、State の更新のために Mutasions のset
を実行したいので、context.commit('set', user)
としています。
上記では説明のためcontext
としていますが、以下のように必要なものだけを参照する方が一般的です。
async fetch({ commit }, id) {
const { data } = await axios.get(`/api/user/${id}`)
commit('set', data)
},
state
とrootState
の違いについては後述します。
参照
コンポーネントで Vuex の情報を参照するには、this.$store
とします。
export default {
computed: {
user() {
return this.$store.state.user
},
userName() {
return this.$store.getters.userName;
}
},
methods: {
async fetchUser() {
await this.$store.dispatch('fetch', this.user.id)
},
asnyc updateUser() {
await this.$store.dispatch('update', this.user)
},
asnyc updateUser()
clearUser() {
this.$store.commit('clear')
}
}
}
Mutations の処理はcommit('メソッド名')
、Actions の処理はdispatch('メソッド名')
として実行します。
middleware
やasnycData
などの場合は、context
オブジェクトのstore
によって参照します。
export default function({ store }) {
//...
}
モジュール分割
上記ではindex.js
を作成して Vuex の定義を行いました。
しかし、管理する状態が増える程 1 ファイルに定義していくのは大変になります。
そこで、状態ごとにファイル分けることを考えます。
NuxtJS であれば、store
フォルダに状態名の JS ファイルを作成するだけです。
例として、user
とtodo
という状態をファイルを分けて作成します。
export const state = () => ({
id: '',
name: ''
})
export const mutations = {
set(state, user) {
state = user
}
}
export const actions = {
fetch({ commit }, id) {
const { data } = await axios.get(`/api/user/${id}`)
context.commit('set', data)
}
}
export const state = () => ({
list: []
})
export const getters = {
count: state => {
return state.list.length
}
}
export const mutations = {
add(state, todo) {
state.list.add(todo)
}
}
以下はそれぞれの参照例です。
index.js
として定義した場合と異なり、各状態名(ファイル名)が必要になる点に注意してください。
//State
this.$store.state.user.id
this.$store.state.todo.list
//Getters
this.$store.getters['todo/count']
//Mutations
this.$store.commit('user/set', this.user)
this.$store.commit('todo/add', this.todo)
//Actions
this.$store.dispatch('user/fetch', this.id)
context に関する補足
説明を保留していた、state
とrootState
の違いについてです。
state
は自身の State であり、rootState
はすべての State になります。
例えば上記のuser
の場合、state
としてはid
とname
が参照できます。
rootState
を用いれば、rootState.todo.list
のようにすべての状態が参照できます。
getters
、rootGetters
についても同様です。
Map ヘルパー
Vuex は、上述したようにstore
によって参照します。
しかし、これを 1 つ 1 つコンポーネントのcomputed
やmethods
に割り当てるのは面倒です。
そこで役に立つのが Map ヘルパーです。 各役割ごとに Map ヘルパーが用意されており、以下のようにコンポーネントへの割り当てを簡易的に行うことができます。
mapState
import { mapState } from 'vuex'
export default {
computed: {
...mapState({
user: (state) => state.user, //this.user
todos: (state) => state.todo.list, //this.todos
}),
},
}
mapGetters
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters({
count: 'todo/count', //this.count
}),
},
}
mapMutations
import { mapMutations } from 'vuex'
export default {
methods: {
...mapMutations({
setUser: 'user/set', //this.setUser(user)
addTodo: 'todo/add', //this.addTodo(todo)
}),
},
}
mapActions
import { mapActions } from 'vuex'
export default {
methods: {
...mapActions({
fetchUser: 'user/fetch', //this.fetchUser(id)
}),
},
}