[Vue.js] Vuexの使い方を知る

17 min read

自分の理解のために再構成しつつ記載している。

Flux のアーキテクチャを知る

Vuex は、Flux(+ReduxThe Elm Architecture)から影響を受けているため、Flux のアーキテクチャを知れば、Vuex を理解しやすくなるだろう。

Flux は、2014 年に Facebook が提唱したアプリケーションのデータフローを管理するためのパターンである(フレームワークなどではなくアプリケーションアーキテクチャ)

Flux の概念として、「データの流れが単一方向にである」を念頭におくことが重要である。

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

Flux の構成要素とデータの流れ

Flux において、データの流れは単一方向である。

データの流れが単一方向という制約で構築することで、どこで状態の変更が起きているのか判別しやすくなり、またコンポーネント間で状態の変更を検知する必要が無くなり、状態管理が容易になる。

また、Flux は下記の 4 つの要素で構成される。

  • Action
    実行する処理を定義する
  • Dispatcher
    Action を受け取り、自身と紐づく Store へ送る
  • Store
    状態を定義、保持し、Dispatcher からの Action に応じて状態を更新する
  • View
    Store の状況に応じて画面を表示・更新する

Data flow within Flux application

Flux において、データフローは Dispatcher に集約される。

Dispatcher は、 Action を Store に送信する役割を持つ。Store は受け取った Action を基に状態を更新し、Store に紐付いた View は画面を更新するといったフローになる。

データの流れは単一方向なので、View から Store へ直接変更を加えることはできないが、View から Dispatcher に対して Action を渡すことで Store に変更を加えることはできる。「View から Store へ変更を加えなければならない」ケースでも単一方向という制約を破らないようにすることでデータフローを複雑化させることを避けられる。


Action

Action は、アプリケーションの内部 API を定義する。アプリケーションと相互作用する方法をキャプチャします。これらは、「タイプ」フィールドといくつかのデータを持つ単純なオブジェクトです。

Action は、発生する Action を意味的に説明するものでなければならない。Action の実装の詳細を記述するべきではない。delete-user-idclear-user-datarefresh-credentialsに分割するのではなく、delete-userという単位で記述する。

すべての Store が Action を受け取り、同じdelete-userAction を処理することで、「データをクリアする」「資格情報を更新する」必要があることが分かるようになる。

Dispatcher

Dispatcher は、Action を受け取り、Dispatcher に登録されている Store を更新する。

Dispatcher は、アプリケーション内で単一であるため、すべての Action が必ず単一 Dispatcher を経由して Store を更新する事が保証される。

Store

Store は、アプリケーションのデータを保持するものである。

Store は、Action を受信できるように Dispatcher に登録しておき、Public な Setter は持たせず、Getter のみを存在させる。Dispatcher が Action を受けて、データに変更が必要であれば、「変更」イベントを発火させる。

View

Store のデータは View に表示させる(View は任意のフレームワークを使用できる)

View が Store からのデータを使用する場合、その Store からのイベントを変更するためにも Subscribe しておく必要がある。 その後、Store の変更を検知して、View は新しいデータに基づき再レンダリングする。

Vuex とは

公式によると

Vuex は Vue.js アプリケーションのための 状態管理パターン + ライブラリです。 これは予測可能な方法によってのみ状態の変異を行うというルールを保証し、アプリケーション内の全てのコンポーネントのための集中型のストアとして機能します。

状態管理パターンとは

Vuex 公式ページに記載されているカウンターアプリのサンプル:

new Vue({
  // state
  data() {
    return {
      count: 0,
    };
  },
  // view
  template: `
    <div>{{ count }}</div>
  `,
  // actions
  methods: {
    increment() {
      this.count++;
    },
  },
});

  • State(情報): アプリの情報源(the source of truth)
    count: 0
  • View(ビュー): State(状態)を View にマッピングするための宣言
    {{ count }}
  • Actions(アクション): View の変更に応じて、State(状態)の変更を可能にする
    increment()

デメリット

しかしながら、上記のような状態管理は複数のコンポーネントから State を共有する形になった場合に複雑さを増してしまう。

  • 複数の View(コンポーネント)が、共通の State に依存する
    プロパティ(props)として深く入れ子になったコンポーネントに渡していくとコードの見通しが悪くなる(データをバケツリレーしていくことになるため)
  • 異なる View からの Actions で、共通の State を変更する
    親子のインスタンスを直接参照したり、$emit などのイベントを介して複数の状態を変更するパターンは、メンテナンスが困難なコードに繋がる

こういった問題を解消させるため Vuex を使用する。

コンポーネントが共有している State をグローバルシングルトンで管理することで、どのコンポーネントであっても State にアクセスしたり、アクションをトリガーすることが容易にできる。また、状態管理を定義、分離し、特定のルールを敷くことで、コードの構造と保守性を向上させることができる。

Vuex の構成要素とデータの流れ

Flux の構成要素が、「Action」「Dispatcher」「Store」「View」だったのに対して、Vuex は下記の 4 つで構成される。

  • Actions
  • Mutations
  • State
  • Getter

Vuex のデータの流れ

データの流れとしては、 Actions → Mutations → State(→ View)のフローである。

  1. Flux と同様に、単一方向のデータフローになる
  2. 状態(State)は Mutations からの commit で変更以外は変更することはできない

Actions

  • Actions は、状態は変更せず、 Mutations を commit する
    Mutations を commit することで状態(State)を更新する
  • Actions は、非同期処理が可能

定義:

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

Actions は、store.dispatch がトリガーとなり実行される。

store.dispatch("increment");

Actions は非同期処理ができるため、そういった処理を含む場合は Actions に記述する。例えば、非同期でバックエンドとの通信(API)を伴うような処理が必要な場合に使用されることが多いように思う。

非同期な API の呼び出しと複数の Mutations の commit の:

actions: {
  checkout ({ commit, state }, products) {
    // 現在のカート内の商品を保存する
    const savedCartItems = [...state.cart.added]
    // チェックアウトのリクエストを送信し、楽観的にカート内をクリアする
    commit(types.CHECKOUT_REQUEST)
    // shop API は成功時のコールバックと失敗時のコールバックを受け取る
    shop.buyProducts(
      products,
      // 成功時の処理
      () => commit(types.CHECKOUT_SUCCESS),
      // 失敗時の処理
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}

Mutations

Mutations は State を更新する唯一の方法であり、同期的である必要がある。
(Actions は非同期可能)

直接、 Mutations を呼び出すことはできない。
タイプを指定して Mutations に登録してある関数を store.commit()を経由で呼び出す必要がある:

// 呼び出し
store.commit('increment')

// 定義
mutations: {
  increment (state) {
    state.count++
  }
}

Actions と Mutations が分けられている理由(余談)

どういう処理をどちらに記述すれば良いのか。公式によると、

状態変更を非同期に組み合わせることは、プログラムの動きを予測することを非常に困難にします。例えば、状態を変更する非同期コールバックを持った 2 つのメソッドを両方呼び出すとき、それらがいつ呼び出されたか、どちらが先に呼び出されたかを、どうやって知ればよいのでしょう?これがまさに、状態変更と非同期の 2 つの概念を分離したいという理由です。Vuex では全てのミューテーションは同期的に行うという作法になっています

非同期の場合、いつ実行されるかが保証されないため、(Mutations を)同期的にすることで状態の変化を予測できるようになる。
こうした理由から明示的に分けるために分けて考えられている。

  • Actions: 非同期
  • Mutations: 同期(状態変更)

役割に応じて明示的に記述法が変わるため、見通しが良くなるように思う。

State

引用文の通り。

Vuex は 単一ステートツリー (single state tree) を使います。つまり、この単一なオブジェクトはアプリケーションレベルの状態が全て含まれており、"信頼できる唯一の情報源 (single source of truth)" として機能します。これは、通常、アプリケーションごとに 1 つしかストアは持たないことを意味します。

単一の State に全てのデータを格納していく。

Getters

State のデータを参照するために使用する。

直接、State を参照するのではなく、State のデータに加工が必要な場合(filterなどの処理)に使用する。

getters: {
  doneTodos: (state) => {
    return state.todos.filter((todo) => todo.done);
  };
}

もし、filterなどの処理をしたデータを複数のコンポーネントで使用したい場合、複数の各コンポーネント側にfilter処理が必要になる(コピーもしくは Util な関数に定義してインポートする必要がある)。
そのような場合において、Getters 内で処理を定義する。算出プロパティと同様に、Getters の結果は、その依存関係に基づいて計算され、依存関係の一部が変更されたときにのみ再評価されるメリットがある。

Vuex 使用所管

いつ、Vuex を使うべきか

公式にはこう書いてある。

Vuex は、共有状態の管理に役立ちますが、さらに概念やボイラープレートのコストがかかります。これは、短期的生産性と長期的生産性のトレードオフです。

もし、あなたが大規模な SPA を構築することなく、Vuex を導入した場合、冗長で恐ろしいと感じるかもしれません。そう感じることは全く普通です。あなたのアプリがシンプルであれば、Vuex なしで問題ないでしょう。

アプリケーションが複雑ではない場合(コンポーネント同士の状態が影響し合わない)、Vuex が不要なケースはあるし、そういった場合は不要だと思う。

ただ、アプリケーションがシンプルな状態で完成する場合は未導入でも良いが、今後も成長する可能性があるのであれば、Vuex は構築時点で導入しておく方が良いと個人的には思う。イチから導入する際のコストは、途中から導入するより遥かに少ないし、Vue.js のエコシステム(Vue CLINuxt.js など)の利用であればもっと簡単に導入が出来るため、公式の引用にあるようなコストは掛からないだろう。

Vuex の使いどころ

すべてのデータを Vuex で管理する必要はない。

データの種類によって「Vuex で管理するのか否か」を区別すれば良い。
例えば、1 つのコンポーネント内でしか使用しない情報は、無理に Vuex で管理せず、コンポーネント内の data での管理で十分だ。

  • アプリケーション全体で使用されるデータ
    → Vuex で管理
  • コンポーネント内のみで使用されるデータ
    → コンポーネントの data で管理

個人的には、「すべてのデータを Vuex で管理する」ことはおすすめしない。

そうした場合、規模感が大きくなるにつれ、Store の管理が複雑になってくる。コンポーネント内で watchcommit が走るとコンポーネント間の状態管理まで複雑化していく。
さらに複数人で開発を行っていくと「状態」以外のもの(定数など)も State に定義し始めるケースも散見された(Store をグローバル変数として扱っているケース)。適切な状態を維持するためにも、Store はシンプルな構成で定義できるように、可能な限り、「すべてのデータを Vuex で管理しないようにする」のが全体の見通しを良くできるように思う。