2018.08.13

Vuexを使ったローディング表示の実装方法


この記事では、Vuex の概要と Vuex のユースケースサンプルとしてローディング表示の実装方法を紹介します。

Vuex とは

状態管理

Vuex とは、Vue.js アプリケーションのための状態管理ライブラリです。「状態管理」と言ってもよく分からないと思いますので、少し説明します(Vuex がどんなものかは分かってるよ!という方は「ローディング表示を作ってみる」まで飛ばしてください)。

まず「状態」とは何の状態かというと、アプリケーションの状態です。たとえばローディングやモーダルウィンドウが開いているか閉じているかも「状態」ですし、ログイン中のユーザーの情報も「状態」です。ToDo アプリであればリストの一覧も「状態」です。状態とは、アプリケーションの UI(見た目)や機能(できること)に影響するデータの値とも言えるでしょう。

なぜ「状態」という言葉が使われているかと言いますと、上述のようなデータの値は変化するからです。時間の経過やユーザーのアクションによって、さっきのデータの値と今のデータの値が違ってくるので「状態」という表現が使われています。

なぜ Vuex が必要か

さてここで問題になるのは、アプリ全体に影響を及ぼすデータの変化をどのように通知するか、です。つまり、コンポーネントAで生じたデータの変化をコンポーネントBやCにどのように通知するかということです。

こちらの記事でも書いたように、コンポーネント間のコミュニケーション方法は「親から子への props」と「子から親への $emit」が基本的な手段となります。

Components 1

ツリー構造が浅く分岐も少ないシンプルなアプリであればあまり問題はないかもしれませんが、規模が大きくなってくるにつれて props$emit では辛くなってきます。

たとえば以下のコンポーネントツリーを想像してください。Bravo コンポーネントで発生した状態の変更(ユーザーがボタンを押したなど)によって Delta コンポーネントの UI を変更する必要があるとします。

Components 1

props$emit で対処すると、上の図のように何度も props$emit でデータをバケツリレーしなくてはいけません。しかも途中の Alpha コンポーネントや Charie コンポーネントの UI は変更する必要がないとしても単に経由させるためだけのコードを記述することになります。赤で書かれているように、Bravo から Delta に直接データを受け渡すことはできません。

このようなデータの受け渡し・通知のフローをシンプルにするためのライブラリが Vuex です。

Components 2

Vuex を導入すると、上図の通り Store(ストア)と呼ばれる「状態の入れ物」を利用することができます。このパターンではアプリ全体に影響する「状態」をそれぞれのコンポーネントがバラバラに持たずに、Store で一元管理します。そうすることで簡潔な記述が可能になります。

ローディング表示を作ろう

説明はこれくらいにして、Vuex のユースケースの一例としてローディング表示を実装してみましょう。

CodeSandbox

サンプルアプリはこちらです。画面が小さくて見づらければ "Edit on CodeSandbox" をクリックしてください。CodeSandbox のページが別タブで開きます。

どのコンポーネントが画面上のどの部分の UI に該当するのか、どのボタンを押したら何が起こるかは確認しておいてください。以降でポイントごとに説明していきます。

ストアオブジェクトを生成する

まずはストアオブジェクトの生成について説明します。store.js をご覧ください。

store.js(抜粋)
new Vuex.Store({
  state: {
    loading: false
  },
  mutations: {
    setLoading(state, payload) {
      state.loading = payload;
    }
  }
});

Vuex.Store がストアオブジェクトを生成するコンストラクタです。今回はこのコンストラクタの引数に2つのオプションを指定しています。statemutations です。

state は、その名の通り「状態」の入れ物です。中にはどのような値も入れられます。アプリケーションに応じて必要な状態を入れておきます。今回はローディングが表示されているかどうかを表す loading という状態を用意しました。

mutations は、状態を変更・更新するためのメソッドです。オブジェクト指向的に言うとセッターメソッドでしょうか。コンポーネント側では状態を参照することはできるのですが、直接書き換えることはできません。mutations が窓口になって状態を変更します。

ほかにも getteraction といった オプションがあります。今回は最小限のセッティングとして statemutations のみ使用しました。

さて、生成されたストアオブジェクトは main.js でインポートして Vue のコンストラクタに渡しています。これによりアプリ内でストアを利用できるようになります。

main.js(抜粋)
import store from "./store";

new Vue({
  store,
});

ツリー構造

次にアプリの構成を把握するために、App.vue を見てみましょう。

App.vue(抜粋)
<template>
  <div id="app">
    <h1>Sample App</h1>
    <HelloWorld />
    <HelloCat />
    <Loading />
  </div>
</template>

コンポーネントツリーを図式化すると以下のようになります。

Components 2

詳しくは後ほど説明しますが、HelloWorld コンポーネントと HelloCat コンポーネントがストアに対して状態の更新を行います(これを commit と呼びます)。Loading コンポーネントはストアの中の loading の状態を算出プロパティで参照しているので、状態が変更されるとローディングの UI を表示させます。

ローディングを呼び出す

さて続いてローディングを表示させる箇所を見ていきましょう。HelloWorld コンポーネントで「load」ボタンをクリックしたときに実行されるのが以下の onLoadClick メソッドです。HelloCat コンポーネントの「load」ボタンでも同様ですね。

HelloWorld.vue(抜粋)
async onLoadClick() {
  this.$store.commit("setLoading", true);
  this.message = await this.loadMessage();
  this.$store.commit("setLoading", false);
},

各コンポーネントでは this.$store でストアにアクセスすることができます。状態を変更するためには、commit メソッドを実行します。commit が行うのは、ストアの生成時に設定した mutation メソッドの呼び出しです。commit メソッドの第一引数は mutations の中のメソッド名で、第二引数はそのメソッドに渡す第二引数です(少しややこしいですね)。

mutation メソッドは以下のように必ず第一引数に state(状態)を取ります。そして第二引数が commit から渡されます。

store.js(抜粋)
new Vuex.Store({
  mutations: {
    setLoading(state, payload) {
      state.loading = payload;
    }
  }
});

最初はどことどこが紐づいているか、パズル感覚で理解しましょう。

ローディングが表示される

最後にローディングが表示される箇所を見ていきます。

Loading.vue
<template>
  <div class="loading" v-show="$store.state.loading">
    <!-- 中略 -->
  </div>
</template>

この Loading コンポーネントでも this.$store でストアにアクセスしています(ただし template 内なので this は記述しない)。ここでは状態を参照するだけなので特別メソッド呼び出しなどは必要なく普通に state.loading にアクセスしています。

まとめ

全体の処理の流れを図示すると以下のようになります。

Components 4

ストアを使って実装することで、$emitprops によるデータのバケツリレーや、それぞれのコンポーネントにいちいちローディングコンポーネントを置くことなく、どのコンポーネントからでもローディング表示を呼び出せるようになりました。

今回は比較的シンプルなユースケースを紹介しましたが、これをきっかけに Vuex を活用してもらえたら嬉しいです。