mintsu's blog

Nuxt.js で Vuex を使う

2019-04-21 00:00:00
フロントエンド

Nuxt.js で Vuex を使う

目次

Nuxt.js で Vuex を使ってみたので使い方、Vuexとはなにか、自分なりに理解したことをまとめます。

Vuex とはなにか

Vuex は Vue.js アプリケーションのための 状態管理パターン + ライブラリです。

これが Vuex の背景にある基本的なアイディアであり、Flux、 Redux そして The Elm Architectureから影響を受けています。

参考: Vuex とは何か? | Vuex

Flux

Vuex を知る前にまずFluxというアーキテクチャを理解する必要があります。
なぜ Vuex を知る前に、Fluxを知る必要があるかというと、Vuex がFluxの考え方を実装したものであるからです。

Fluxは、アプリケーション内のデータフローを管理するためのパターンです。
Fluxの考え方は、データが一方向に流れるということが重要な概念です。
データが単一方向に流れるように制限することで、データ管理の複雑さを軽減することができます。

参考: flux/examples/flux-concepts at master · facebook/flux

次に図をまじえ、Fluxの各要素について確認していきます。

Fluxの要素

Flux のアーキテクチャは下記図になっています。
Action => Dispatcher => Store => View と前述したようにデータが一方向に流れる様になっていることがわかると思います。

Fluxのアーキテクチャ

図にある通り、Fluxの要素は下記4つになります。

  • Dispatcher
  • Store
  • Action
  • View
Dispatcher

Actionを受け取り、Dispatcherに登録した Store のデータ更新ロジックを発火する。すべてのStoreはすべてのActionを受け取ります。
Dispatcherはシングルトンでアプリケーションに1つだけです。そのため、すべてのActionは1つのDispatcherを通してStoreの更新を発火するいう認識です。

Store

Storeはアプリケーションの状態(state)やデータを保持するところです。
StoreはpublicなSetterは持たず、Getterのみ提供します。
データの変更はどのように行われるかというと、データ変更の関数をDispatherに登録しておき、DispatcherがActionを受け、発火したときにActionのTypeを見てデータ更新の必要があれば更新が行われます。

Action

Actionはアプリケーションの内部APIを定義する。
「ユーザを削除する」 などの行動を定義。
Flux のActionは type フィールドといくつかのデータを持つシンプルなオブジェクトになります。
私の理解としては、イベントだったり、メッセージのようなイメージを持っています。

View

ViewはStoreのデータを表示または利用する。ReactやVueではコンポーネントがViewの役割となる。
ViewはStoreからの変更イベントをSubscribeする。Subscribeしているので、Storeの変更を検知して、Viewの再描画を行うことができる。
実際にはVueやReactのコンポーネントを使う際はリアクティブの機構を持っているため、明示的にSubscribeを意識はしないかもしれない。

また、ViewからActionを送出することもある。例えばユーザが「ユーザ削除」ボタンをクリックしたときとかだ。「ユーザ削除」ボタンがクリックされれば当然「ユーザを削除する」というActionが作られるでしょう。
ViewからActionに伸びている線はこのことを指していると思われる。

Vuex

Vuex のもととなる概念であるFluxについて理解したところで、Vuexについて見ていきます。
Vuex は Vue.js アプリケーションのための状態管理パターン+ライブラリです。

Vuex のデータフロー

Fluxの思想を持っているため、Flux同様に、Action => Mutation => State => Vue Component (View) という、単一方向のデータフローになります。

Vuexのデータフロー

ここで少しわかりにくいのですが、Flux の要素である「Dispatcher」,「Action」、「Store」、「View」と一対一で結びつきませんが単一方向のデータフローであるということが重要かと思います。

Fluxのところでも述べましたが、データが単一方向に流れるように制限することで、データ管理の複雑さを軽減することができます。

Vuex Store の機能

Vuex アプリケーションの中心はストアです。ストアとは、アプリケーションの状態(state)を保持するコンテナです。
Vuex の Store は、アプリケーションに1つしかストアを持ちません。そのため「Single source of truth」として機能します。

Single source of truth とはReduxの原則にあるもで、アプリケーション内の状態を1つの大きなオブジェクトとして管理します。その結果、デバッグやテストが容易になったり、データをどこからでも取り出すことができるので状態管理を行うアプリケーションの実装が楽になります。
実装が楽になるというのは、複数のコンポーネントで状態を参照している場合に、複雑な受け渡し無しで、同じデータソースを参照できる点です。

Vuex Store は以下の4つの機能で成り立っています。
実際にコードを書くときも下記の4つを書くことになります。

  • Action
  • Mutation
  • State
  • Getter
Action

Actionはユースケースの一連の処理の流れなどはここに書いていくのかなと思っています。
また、後述しているMutationとは違い非同期で処理が可能です。
Backend API などからデータを引くような処理はここで実装することになります。
Action自体は状態を変更することはなく、Mutationをコミットすることで状態を更新します。

Mutation

MutaionはSateを更新する唯一の方法です。
Mutationの関数は実は直接呼ぶことはできません。すべて`store.commit()」を利用して、mutationに登録してある関数を呼び出します。
このあたりはReduxの「State in read only」の原則に近いのかなと感じました。

Mutationは同期的でなければなりません。
同期的にすることによって状態の変化を予測可能になります。非同期である場合は、いつ実行されるか保証されないため、順序や、変更時の挙動が予測不能となります。

State

Storeで管理する状態やデータです。Vuex では単一ステートツリー(single state tree)を利用し、stateはアプリケーションで唯一の状態管理オブジェクトとなります。
これにより「single source of truth」が実現されていると理解しています。

Getter

stateのデータを参照するためのGetterメソッドです。
filterなどの処理を行って、view側に返すことができます。
例えば、複数のコンポーネントがfilter処理されたデータを使いたい場合に、コンポーネント側にfilter処理があると、それぞれのコンポーネントでfilterの実装を書く必要が出てきてしまいます。そういった場合に、Getterを定義することによって共通化することができます。

Vuex機能の処理の流れ

上記に説明した機能を、実装時に使う言葉を用いて、処理の流れを書くと図のようになると思います。

Vuex機能の処理の流れ

Nuxt.js で Vuex を利用する

Nuxt.js で Vuex を利用して、アプリケーションを開発します。

Nuxt.jsの場合Vuexは非常に簡単に導入できます。
Nuxt.js では store ディレクトリ以下に、Vuex Storeのファイルを配置します。
storeディレクトリ内のファイルは勝手にVuexストアと認識されるので、特別な設定はなしにすぐに導入可能です。

Nuxt.js では、今はモジュールモードが推奨されているので、今回はモジュールモードでVuex利用します。

今回作るアプリ

足し算の問題を作るというアプリケーションを作ってみたいと思います。

アプリイメージ

問題数を指定して、問題を自動生成するアプリです。

Store ファイルの作成

Nuxt.js では store ディレクトリ以下にStoreを表すJavaScriptのファイルを置くことで、自動的にVueインスタンスに追加してくれます。
また、Nuxt.js ではVuexのモジュールを使うことが推奨されているようで、モジュールモードとして動作させます。

storeディレクトリ以下の *.js ファイルがモジュールになります。

store/moduleA.js
store/moduleB.js

といったファイルを作れば、Vuexのモジュールとして、moduleA,moduleBとして扱われます。 今回は足し算をするアプリを作るので、store/addition.jsというファイルを作ってみます。

storeディレクトリ以下の各ファイルの内容は、下記の様なスクリプトになります。

export const state = () => ({})
export const actions = {}
export const mutations = {}
export const getters = {}

Vuexの説明に書いた「state」、「action」、「mutaiton」、「getter」を持っているのがわかると思います。
ここにそれぞれの処理を追加していくこととなります。

stateの実装

state で持つべき値を決めます。 今回は下記のデータを持つことにします

export const state = () => ({
  questionNum: 0,  // 問題数
  questionList: [] // 問題データセット
})

問題データセットの構造は下記の様なイメージにします

{
  questionList: [
    firstAddend: 0, // 足す数
    secondAddend: 0, // 足す数
    sum: 0 // 答え
  ]
}

mutationの実装

mutation typeに定数を使用するようにします。
定数にしなくても、構わないのですが、定数にしておいたほうが補完が効きうち間違えによるバグが減ったり、linterなどのツールも利用できるようになるメリットがあります。

mutationType.js

export const SET_QUESTION_NUM = 'setQuestionNum'
export const SET_QUESTION_LIST = 'setQuestionList'

Mutation は次のように処理を書いていきます。
Mutation はStateを更新する処理を書きます。

import {
  SET_QUESTION_NUM,
  SET_QUESTION_LIST
} from './mutationType.js'

export const mutations = {
  [SET_QUESTION_NUM](state, questionNum) {
    state.questionNum = questionNum
  },
  [SET_QUESTION_LIST](state, questionList) {
    state.questionList = questionList
  }
}

actionの実装

今回問題数(questionNum)もaction経由で更新するようにしています。
実はコンポーネントからmutationを直接commitすることができますが、フローに合わせるために単純な更新であってもaction経由で呼び出すようにしています。
Vuexではコンポーネントから、action、mutation両方呼ぶことができてしまうので、実際扱うときはすべてaction経由で呼ぶようにするルールを設けたり、両方呼べるというルールは設けたほうが良さそうです。

actionには処理の流れを書きます。
generateQuestionsでは問題生成の処理も含めています。場合によってはここは外部APIから問題を取得するような処理になることもあります。

export const actions = {
  generateQuestions({ commit, state }) {
    const questionList = []
    for (let i = 0; i < state.questionNum; i++) {
      const firstAddend = Math.floor(Math.random() * 100)
      const secondAddend = Math.floor(Math.random() * 100)
      const sum = firstAddend + secondAddend
      questionList.push({
        firstAddend: firstAddend,
        secondAddend: secondAddend,
        sum: sum
      })
    }

    commit(SET_QUESTION_LIST, questionList)
  },
  setQuestionNum({ commit }, questionNum) {
    commit(SET_QUESTION_NUM, questionNum)
  }
}

getterの実装

export const getters = {
  questionList(state) {
    return state.questionList
  }
}

Viewの実装

Viewは使い慣れてるVuetifyを使っています。
7行目の@input="setQuestionNum"でView上の問題数の入力値が変わるタイミングでActionを呼んで問題数のstate(questionNum)を変更してます。
9行目で<v-btn @click="generateQuestions">でクリックしたときに、問題を生成し直して、stateを変更するようにしています。
11行目〜のtemplateタグ内の処理で、でquestionListはgetterでから取得したデータを表示しています。stateが変更されたタイミングで表示も自動で変わります。

<template>
  <v-layout column justify-center align-center>
    <v-flex xs12 sm8 md6>
      <v-text-field
        type="number"
        label="問題数"
        @input="setQuestionNum"
      ></v-text-field>
      <v-btn @click="generateQuestions">
        問題作成
      </v-btn>
      <template v-for="(question, index) in questionList">
        <div :key="index">
          <p>
            {{ question.firstAddend }}
            +
            {{ question.secondAddend }}
            =
            {{ question.sum }}
          </p>
        </div>
      </template>
    </v-flex>
  </v-layout>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'
export default {
  methods: {
    ...mapActions('addition', ['generateQuestions', 'setQuestionNum'])
  },
  computed: {
    ...mapGetters('addition', ['questionList'])
  }
}
</script>

感想

Vuex を使うと、データを一元管理することによって、データの利便性が上がる。
いままではデータの受け渡しはpropsを使ってデータを引き回していました。今回のサンプルではあまり恩恵を受けていませんが、そういった場合にデータを引き回す必要がなくなり、シンプルになそうです。

また、データに関連する処理をStoreに閉じ込められるので、データ操作の処理がコンポーネントに散らばったりすることもなくなるため、コンポーネントの実装も楽になりそうです。

Nuxt.js を使うと非常に簡単に導入できるので、積極的に使っていこうと思います。

参考

flux/examples/flux-concepts at master · facebook/flux

Vuex とは何か? | Vuex

Vuex ストア - Nuxt.js