nuxt.svg

【Nuxt 2】Vuex(Store)

Nuxt2

Vuex とは

Vuex は、Vue.js アプリケーションにおいて「状態」を管理するのためのライブラリです。 この「状態」は、コンポーネントとは別で管理され、すべてのコンポーネントから参照・更新が可能です。

例えば SPA を構築する場合に、すべてのページでユーザー情報が必要だとします。

基本的にページ毎に状態(データ)を持つため、ページ遷移の度に状態はリセットされます。 つまりページ遷移の度に、何かしらの手段を用いてユーザー情報を取得する必要があります。

これを Vuex によって管理するようにします。 前述したように、Vuex はコンポーネントとは別で管理されます。 つまり、ページを遷移したとしても Vuex で管理している状態はそのまま保持されているため、ユーザー情報の取得は必要最低限で済みます。

Vuex の状態はメモリ上で管理されるため、ウィンドウを閉じたり、ページリロードをすると初期化される点に注意が必要です。

Vuex の基本

Vuex では、以下の図のように状態管理が行われます。 以下よりそれぞれの役割について説明します。

参照:https://vuex.vuejs.org/ja/

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ファイルを作成します。 以下はユーザー情報を管理するための簡単な例になります。

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になります)。

具体的に、idnameをデータとして渡したい場合は、{ id, name }のようにまとめる必要があるということです。 当然メソッドは、user.iduser.nameのように参照する必要があります。

Actions の context は、Vuex の情報

Actions の第二引数であるcontextは、以下 Vuex の情報を持ちます。

  • state:自身の State
  • getters:自身の Getters
  • commit: Mutations の実行
  • dispatch:Actions の実行
  • rootState:すべての State
  • rootGetters:すべての Getters

今回は、State の更新のために Mutasions のsetを実行したいので、context.commit('set', user)としています。 上記では説明のためcontextとしていますが、以下のように必要なものだけを参照する方が一般的です。

async fetch({ commit }, id) {
  const { data } = await axios.get(`/api/user/${id}`)
  commit('set', data)
},

staterootStateの違いについては後述します。

参照

コンポーネントで 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('メソッド名')として実行します。

middlewareasnycDataなどの場合は、contextオブジェクトのstoreによって参照します。

middleware
export default function({ store }) {
  //...
}

モジュール分割

上記ではindex.jsを作成して Vuex の定義を行いました。 しかし、管理する状態が増える程 1 ファイルに定義していくのは大変になります。 そこで、状態ごとにファイル分けることを考えます。 NuxtJS であれば、storeフォルダに状態名の JS ファイルを作成するだけです。

例として、usertodoという状態をファイルを分けて作成します。

store/user.js
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)
  }
}
store/todo.js
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 に関する補足

説明を保留していた、staterootStateの違いについてです。 stateは自身の State であり、rootStateはすべての State になります。

例えば上記のuserの場合、stateとしてはidnameが参照できます。 rootStateを用いれば、rootState.todo.listのようにすべての状態が参照できます。

gettersrootGettersについても同様です。

Map ヘルパー

Vuex は、上述したようにstoreによって参照します。 しかし、これを 1 つ 1 つコンポーネントのcomputedmethodsに割り当てるのは面倒です。

そこで役に立つのが 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)
    }),
  },
}