主夫ときどきプログラマ

プログラミング、Webエンジニアリング、etc

Vuexのはじめかた - Vuex入門者向けのはじめの一歩ガイド

これはVue.jsアドベントカレンダー#2 14日の記事です。

Vuexはflaxアーキテクチャを採用した公式プラグインで、vue.jsで利用することができます。 公式の日本語ドキュメントもあるので導入しやすい状況になっています。

とはいえ、いざ使ってみようという時には気が引けてしまうもの。 自分が導入を試みた時もいろいろと悩むポイントがありました。

  • どこから手をつけていいかわからない
  • ファイル構成のお手本が知りたい
  • スタンダードな書き方を知りたい

このあたりのことを、どう解決したかについて今回は紹介します。

はじめにすること

公式ドキュメントを読みましょう。苦手な人もいると思いますが なにを意識しながら読むかが決まっていれば読みやすくなるものです。

fluxを理解する

事前にfluxアーキテクチャについて理解する必要があります。fluxを知った上でVuexのドキュメントを読むとより理解しやすくなります。 アーキテクチャの構成要素と一方向性の特徴がつかめる程度でOKです。

このあたりの記事に解説があります。

WEB+DB PRESS Vol.97があれば特集記事があります。一番わかりやすいです。

公式ドキュメントを読む

Vuex とは何か?

事前に公式ドキュメントを一通り読んでおきましょう。流し読みでよいです。 コアコンセプトに具体的な実装方法が記載されているので、これを中心に読むと良いでしょう。 初めての時は書き方を覚えることもできないし、ぼんやりとしか理解できないと思います。 でもそれでOKです。 全体として何が出来るのかをつかむことが重要です。

  • Vuex とは何か?
  • Vuex 入門
  • コアコンセプト

の3つの章を読めばとりあえずは大丈夫です。

小さく始めよう

コアコンセプトにある5つの要素をいきなり全て取り入れることは、初心者にとって困難なことです。 そうなった時は、何から手を付けていいかわからないという状況に陥ります。 なので小さく始めて少しずつVuexを取り入れていきましょう。

サンプルコードは $ vue init webpack sample で作成されたプロジェクトを基に進めていきます。

まずはステートから

まずはステートの導入から初めましょう。以下のような何でもない Sample.vue を例にします。
ステートのドキュメント

// components/Sample.vue
<template>
  <div class="sample">
    <h1>{{ msg }}</h1>
  </div>
</template>

<script>
export default {
  name: 'sample',
  data() {
    return {
      msg: 'Welcome to My Vuex Sample',
    };
  },
};
</script>

Vuex ストアを作成

msg プロパティを表示するだけのシンプルなコンポーネントです。この msg プロパティを Vuex に移していきます。 まず、Vuexストアを作成しVueに入れます。

// main.js
import Vuex from 'vuex';
Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    msg: 'Hello Vuex Store.',
  },
});

new Vue({
  el: '#app',
  router,
  template: '<App/>',
  components: { App },
  store,
});

これでVuexを使う準備が整いました。

コンポーネントから Vuex ストアを参照する

次はVuexストアから msg ステートを読み込んで Sample.vue に表示しましょう。 コンピューテッドプロパティからストアを参照するように変更します。

// components/Sample.vue
export default {
  name: 'sample',
  data() {
    return {
      // msg: 'Welcome to My Vuex Sample.',
    };
  },

  computed: {
    msg() {
      return this.$store.state.msg;
    },
  },
};

Hello Vuex Store. が表示されましたね!おめでとう!

mapState ヘルパーを使う

msgのようなステートを複数読み込む場合には mapState ヘルパーを使うことで簡潔に記述することができます。

// components/Sample.vue
import { mapState } from 'vuex';

export default {
  name: 'sample',
  data() {
    return {
    };
  },

  computed: {
    ...mapState({
      msg: state => state.msg,
    }),
  },
};

msg の長さを取得するコンピューテッドプロパティを定義したい場合にもローカルステートのようにthisでアクセスできます。

// components/Sample.vue
computed: {
  msgLength() {
    return this.msg.length;
  },
  ...mapState({
    msg: state => state.msg,
  }),
},

これでVuexのステートを利用することが出来るようになりました。

ステートの値を変更するには

ステートの msg を更新するにはどうしたら良いでしょうか。更新ボタンをクリックすると、フォームの入力値でステートを更新するようにします。

// components/Sample.vue
<input type="text" v-model="newMsg" />
<button @click="update">更新</button>

export default {
  name: 'sample',
  data() {
    return {
      newMsg: null,
    };
  },

  methods: {
    update() {
      this.$store.state.msg = this.newMsg;
    },
  },
};

update() メソッドで msgnewMsg に入力された値に更新します。 しかしこの方法だとアプリケーションが大きくなるにつれ、どこで msg が更新されるかが追いづらくなります。 そこでミューテーションの出番です。

ミューテーションを定義する

Vuex のストアの状態を変更するためにミューテーションを定義します。
ミューテーションのドキュメント

// main.js
const store = new Vuex.Store({
  state: {
    msg: 'Hello Vuex Store.',
  },
  mutations: {
    updateMsg(state, newMsg) {
      state.msg = newMsg;
    },
  },
});

この時 updateMsg をタイプ、定義された関数をハンドラと呼びます。updateMsg というイベントを登録するようなものです。 これを起動するには store.commit('updateMsg') を呼び出す必要があります。
air-bnb のESLintルールにしているとエラーがでるので .eslintrc.js"no-param-reassign": 0 を追加します。

// components/Sample.vue
methods: {
  update() {
    // this.$store.state.msg = this.newMsg;
    this.$store.commit('updateMsg', this.newMsg);
  },
},

タイプに定数を使う

updateMsg のようなタイプに定数を使うのがVuexのスタンダードなのでそれにならいましょう。

// main.js
const UPDATE_MESSAGE = 'UPDATE_MESSAGE';

const store = new Vuex.Store({
  state: {
    msg: 'Hello Vuex Store.',
  },
  mutations: {
    [UPDATE_MESSAGE](state, newMsg) {
      state.msg = newMsg;
    },
  },
});

こうなると、commit する箇所でも同じ定数を使う必要になるので外部ファイルに定義をしましょう。 store/mutation-types.js を作成し底定数を定義します。 main.js ではそのファイルを import します。

// store/mutation-types.js
export const UPDATE_MESSAGE = 'UPDATE_MESSAGE';
export const DUMMY_TYPE = 'DUMMY_TYPE';  // ESLint対策、exportが1つだけだとErrorがでます
// main.js
import * as types from './store/mutation-types';

mutations: {
  [types.UPDATE_MESSAGE](state, newMsg) {
    state.msg = newMsg;
  },
},
// components/Sample.vue
import * as types from '@/store/mutation-types';

methods: {
  update() {
    this.$store.commit(types.UPDATE_MESSAGE, this.newMsg);
  },
},

アクションを利用する

アクションはミューテーションと似ていますが、状態を変更しない点が異なります。 APIコールなどの非同期処理を含むことができ、ミューテーションをコミットすることで状態を変更します。 文字列を繰り返すアクションを登録してみましょう。

// main.js
const store = new Vuex.Store({
  actions: {
    repeat(context) {
      let msg = context.state.msg
      context.commit(types.UPDATE_MESSAGE, `${msg} ${msg}`)
    },
  }
});

context.state で状態にアクセスし、 context.commit を呼び出すことでミューテーションにコミットできます。

アクションのディスパッチ

アクションは store.dispatch により実行されます。 「繰り返す」ボタンをクリックされた時にアクションをディスパッチします。 すると repeat アクションが実行され、表示が更新されます。

// components/Sample.vue
<button @click="repeat">繰り返す</button>

methods: {
  repeat() {
    this.$store.dispatch('repeat')
  },
},

これでVuexを使った一連のコードを実装することできました。

ファイルの分割とモジュール化

main.js に実装したVuexのコードを専用のファイルに分割しましょう。 store/index.js を作成しそちらにコードを移します。 ミューテーションやアクションもそれぞれ別の変数で管理します。

// store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
import * as types from './mutation-types';

Vue.use(Vuex);

const state = {
  msg: 'Hello Vuex Store.',
};

const mutations = {
  [types.UPDATE_MESSAGE](state, newMsg) {
    state.msg = newMsg;
  },
};

const actions = {
  repeat(context) {
    let msg = context.state.msg;
    context.commit(types.UPDATE_MESSAGE, `${msg} ${msg}`);
  },
};

export default new Vuex.Store({
  state,
  mutations,
  actions,
});
// main.js
import store from './store';

new Vue({
  store,
  el: '#app',
  router,
  template: '<App/>',
  components: { App },
});

機能ごとにファイルが別れて管理しやすくなりました。これでVuexの基本的な機能を使って実装することができました!

アプリケーションの構成

ファイル構成についてはアプリケーションの構成で言及されこのようになっています。

├── index.html
├── main.js
├── api
│   └── ... # API 呼び出しを抽象化する
├── components
│   ├── App.vue
│   └── ...
└── store
    ├── index.js          # モジュールを集めてストアをエクスポートする
    ├── actions.js        # アクションのルートファイル
    ├── mutations.js      # ミューテーションのルートファイル
    └── modules
        ├── cart.js       # cart モジュール
        └── products.js   # products モジュール

サンプルコードを読んでスタンダードを知る

最後にVuexを使ったプログラムのスタンダードな書き方についてです。 Vuexのリポジトリではサンプルコードが公開されています。 shopping-cartのサンプルは上記のようなアプリケーション構成になっていてとても参考になります。

store の構成に注目すると、問題領域ごとにmodule化され、アクションやミューテーションが定義されています。 また、ミューテーションを区別するミューテーションタイプは一つのファイルに定数として保存され、重複しないようになっています。

最後に

はじめてアドベントカレンダーに参加しましたが、なんとか書き終えてホッとしています。 この記事で使用したサンプルコードはこちらのリポジトリです。 ESLintの設定にAir-bnbを指定しましたが、いくつかの設定でエラーとなってしまったので、webpackの設定をおすすめします。