初めに
こんにちは。mako_makokです。
フロントを Vue + Vuex + TypeScript で新規開発中のSaaSプロダクトに携わることになり、急ピッチでVueを学習しています。
今回はVuexについて学習したので、その結果をまとめました。
Vuexは一言で表すと、単一方向のデータフローを持った状態管理パターンのライブラリです。
記事内でサンプルが登場しますが、Vueのバージョンは2系で書いています。
Vuexをなぜ使うのか?
Vueアプリケーションを開発しているとアプリケーションの状態を参照し、コンポーネントの動作を変えたくなることがあります。 例えば、カレンダーアプリなどでは"現在表示する月"を状態として持ち、"現在表示する月"を参照して表示する日付・日数・予定を変えたくなると思います。
このような動作をする処理を愚直に実装すると、状態自体が複数のコンポーネントに分散してしまったり、状態を変更するロジックが分散、重複してしまうケースが考えられます。
では状態を抽出し、グローバルなオブジェクトで持てば解決するのでは?と考えるかもしれません。 ですが、これはコンポーネント分割している意味がありません。コンポーネントツリーのどこからでもアクションを実行することや、状態を変化させることができてしまいます。それはもはや巨大なコンポーネントが出来上がるのと同義です。
そこで、状態管理に関わる部分を定義・分離・ルール化してコードの構造と保守性の向上を目的としたライブラリがVuexです。
Fluxについて
Vuexの話を続けていきたいところですが、Vuexの公式には以下のようにあります。
Flux、 Redux そして The Elm Architectureから影響を受けています。
最初に書いた、単一方向のデータフローを持った状態管理パターンというのはFluxに特に影響を受けています。 まず最初にFluxを知ることによって、Vuexをより理解しやすくなるのではと思います。
Fluxとは、Facebookが提唱しているアーキテクチャパターンです。 以下はFacebookが実際にFluxの実装を行ったサンプルリポジトリです。
FluxではMVCやMVVMと異なり、単一方向のデータフローを有していることが特徴です。
具体的には以下の図のようになります。
- Store
- アプリケーションのデータ(状態)を保持する
- Dispatcherから更新される
- publicなsetterを持ってはならない
- publicなsetterがあると、無秩序にStoreを書き換えることができてしまう
- Dispatcher
- Actionを受け取ってStoreを更新する
- すべてのアクションを受け取る
- Action
- Storeをどのようなロジックで更新するか記載した命令*1
- View
- VueやReactで言うところのコンポーネント
正直これはだけではパっとしないと思うのでToDoアプリでToDoを追加する動作を例にとってみます。
- アプリケーションはToDoを保持しているStoreを持っている
- ToDoを入力し、追加ボタンを押すとToDoを追加するActionがDispatcherに命令
- 命令を受け取ったDispatcherがActionを実行し、StoreにToDoを追加する
- StoreにToDoが追加されたので、更新を検知したViewは再レンダリングを行い、追加されたToDoを表示する
ActionはToDoを直接更新せず、Dispatcherに命令を渡しているところがミソとなります。
以上がFluxの概要になります。ここでポイントとなるのは以下の2点です。
- Fluxは単一方向のデータフローをもったアーキテクチャである
- StoreはActionを受け取ったDispatcherからでしか更新できない
Vuexのアーキテクチャ
いよいよVuexのアーキテクチャについて説明します。以下図はVuexのアーキテクチャです。
FluxからState, Getter, Mutationが増えています。また単一方向のデータフローを持っていることがわかると思います。 VuexではFluxで登場したStoreやDispatcherとコンテキストが微妙に変わっている箇所があります。
- Store → Vuexのインスタンス
- State → FluxでいうところのStore
- Getter → Stateから値を取得する
- Mutation → Stateを更新する。FluxのDispatcherの更新部分だけを切り離したようなイメージ
- Dispatch → Actionを実行する役割に特化
- Action → Stateをどのようなロジックで更新するか記載した命令
以上が簡単なアーキテクチャの説明です。なんとなくFluxから言葉が変わったり、Dispatcherの役割が分かれたんだなーくらいの感覚を持っていただければ大丈夫です。 今回はToDoアプリケーションを作りながらそれぞれ説明していきます。
Store
StoreはVuexのインスタンスです。主にstate, getters, mutations, actionsから構成されます。 詳しく知りたい方はVuex.StoreのAPIリファレンスを御覧ください。
const store = new Vuex.Store({ state: {}, getters: {}, mutations: {}, actions: {} }
重要なのはStoreはアプリケーション内で常に1つであるように設計するということです。 理由は後述します。
State
アプリケーションの状態を保持します。Stateは信頼できる唯一の情報源でなくてはなりません。 Storeの項で、Storeはアプリケーション内で常に1つであるように設計すると述べました。 Storeが一つということはStateも一つしかないことが担保されますので、Stateは"信頼できる唯一の情報源"が実現します。
Stateは以下のようにオブジェクトを記述していきます。
state: { todos: [] },
Getter
GetterはStateの一部や、Stateで計算された値を返します。 Getterの特徴として、Getter内での計算結果はキャッシュされます。 キャッシュされたGetterの結果は、Stateが更新されるとクリアされます。
今回はStateから完了済みのToDoを返すGetterを作成しました。
getters: { doneTodos: state => { return state.todos.filter(todo => todo.done) } }
Mutation
MutationはStateを更新する唯一の手段です。 Mutationにはいくつかのルールがあります。
- Mutation以外がStateを更新することは禁止
- Mutationは直接参照しない。
Store.commit()
から呼び出す - Mutationには非同期処理を書いてはならない
- devtoolでmutationの更新履歴を追いかけづらくなる
- 意図しないタイミングで更新処理が入る
VueのdevtoolではStateの更新履歴を追うことができるのですが、Mutationで非同期処理を書いてしまうと 意図しないタイミングで更新され、どのようにStateが書き換わったかわかりづらくなってしまうので規約レベルで禁止されています。 以上のことを踏まえ、Mutationを書いていきます。
MutationにはStateを第一引数に渡します。また、Mutationには追加の引数を渡すことができます。 追加の引数のことをペイロードと呼びます。 storeのオブジェクトを直接弄るように書きます。
mutations: { HOGE (state) { state.hoge = "hoge" }, FUGA (state, payload) { state.fuga = payload.content } }
2のルールに則り、Mutationはstore.FUGAなどと呼び出すことは禁止されています。
Mutationを呼び出すために、以下のようにstore.commit
を使用します。
store.commit('FUGA', { content: 'fuga'})
ToDoリストにToDoをpushする処理を書いていきます。
mutations: { PUSH_TODO(state, {content, done}) { state.todos.push({content: content, done: done}) } }
Mutationを書くときのコツ
余談ですが、MutationではStateを更新する処理を書くべきで、具体的な更新内容を書くべきではありません。
具体的な動作は後述するActionに記述します。
カウンターアプリであれば、例えばStateのcounterを1足したい時increment
という関数はMutationに記述せず、
Mutationには数値を受け取ってcounterを変更するchangeCounter()
を作成します。
Actionにincrement
という関数を定義し、increment
内でchangeCounter
を呼び出し、1を渡してあげましょう。
このように記述することで、decrement
を作成したくなったときも効率的に書くことができると思います。
Action
Actionは最終的にMutationをcommitします。 Mutationでは非同期処理を書くことは禁止されていましたが、Actionには非同期処理を書くことができます。 第一引数にはコンテキスト、第二引数にはMutationと同様ペイロードを渡すことができます。
コンテキストはオブジェクトなので、分割代入で使用するものだけ渡すとスッキリします。
actions: { doHoge (context) { context.commit('HOGE') }, doFuga ({ commit }, payload) { commit('FUGA', payload) } }
Actionは直接呼び出せません。store.dispatch
から呼び出します。
store.dispatch('doFuga', payload)
ToDoアプリのactionは以下のように書いていきます。
図からも分かる通り、APIで通信処理などはここに書きます。
ToDoアプリでいい非同期処理を思いつかなかったのでとりあえず1秒ずらしてToDoを追加します
actions: { addTodo({ commit }, { content }) { commit('PUSH_TODO', { content: content, done: false }) }, addTodoAsync({ commit }, { content }) { setTimeout(() => { commit('PUSH_TODO', { content: content, done: false }) }, 1000) } }
これでToDoアプリのStoreが完成しました。
最後にVueコンポーネントにstoreオブジェクトを渡して完成です。
new Vue({ el: "#app", store,
Vueコンポーネントにstoreを渡しているだけなのでthis.$store
からアクセスできます。
今回は直接storeを代入していますが、webpackを使用している場合はVuexをimportしてきてVue.use(Vuex)
でも同じことができます。
完成形を以下においておきます。
jsfiddle.net
まとめ
ここまでVuexについて書いてきました。 以下にポイントをまとめます。
- Storeはアプリケーションに1つだけ存在するように設計する
- Stateは直接更新しない。必ずMutationから更新すること
- Mutationは直接呼び出さないこと。
store.commit
で呼び出す - Mutationには非同期処理を書かない
- Actionは直接呼び出さないこと。
store.dispatch
で呼び出す
おわりに
基本的に規約を守って書けばVuexのコードを書くのにそこまで苦労はしないかと思います。
Vuexは難しいと聞いていましたが、APIも多くなく基本的な書き方自体はスッと入ってきた気がします。
しかし、本当に難しいと感じたのはそもそもプロジェクトにVuexを入れるべきか*2、どこまでStoreに持たせるか、ということです。
また、Vuex + TypeScriptのツラミやVue3 + Vuexなど色々ネタがあるのでこの辺も記事にしていけたらと思います。
私たちは一緒に働くメンバーを募集しています。
ご興味を持たれましたら以下のサイトからお問い合わせください。