Last Updated: 7/13/2020, 2:40:47 PM

# Vuex はなるべく避ける

Vuex は、グローバル変数 state を使うために使います。 actions, mutations でグローバル変数 state を変更し、getters でグローバル変数 state を参照します。

たかだかグローバル変数 state を変更するだけで、こんなに大きな Vuex というライブラリがあるのか、疑問でした。

Vuex は、「いつ」、「どこで」、「だれが」、変更したのかを監視するためのライブラリです。 また Vue.js devtools を使えば、「いつ」、「どこで」、「だれが」 グローバル変数 state を変更 mutation したかを確認することができます。

これら、いつ、どこで、だれが、変更したのかを監視したいという「気持ち」と、 その気持ちに応えてくれる「ツール」、2つを見落としていて、Vuex の理解が若干苦しいことがありました。

# Vuex はなるべく避ける

そして Vuex は一般になるべく使わない方が良いという記事や、ツイートを見かけます。 大変勉強になりました、ありがとうございます。

だいたい以下のような使い分けかなと個人的に思っています。

◯ Vuex を使ってもよいかもしれない
Page, Template, Organism レベルのコンポーネントの中で
// 参考: Webフロントエンド開発現場を前向きに改善した話

Page レベルのコンポーネントの中で
// 参考: Nuxt.jsプロジェクトの改善テクニック

◯ Vuex を使わない
上記以外
// 参考: vuexで何をするか、何をしないか

Page, Template, Organism は Atomic デザインの用語らしいです。 Vuex の話がなされるときに、どこで Vuex を使ってもよいかを話すときに、 Atomic デザインの話が出てきます。

簡単に抑えておくとよいかなと思いました。 区分けの厳密な定義があるわけではなさそうですが、 だいたいこのくらいの感覚でコンポーネントを分割しておくと、 管理しやすいよね、みたいなそんな話のようです。

# Vuex をなるべく避ける理由

Vuex は、変化するグローバル変数です。 Vuex を避ける理由は、グローバル変数をなるべく避けるのと同じだと思っています。 関連する処理を覚えてる、あるいは把握する必要があります。

まず第一に Single File Component(以下 SFC) が、 その他の SFC と関係を持ってしまうからです。 また第二に SFC が、副作用を持ってしまうからです。 副作用とはオブジェクトのプロパティが変化することです。

# 1. SFC が他の全ての SFC と関係を持ってしまう。

第一の要因については、 props で値を渡してくれれば、 関係を持つ SFC は、呼び出し元の SFC だけになります。 しかし Vuex の state を使ってしまうと、その state を使う SFC の数だけ関係を持つ SFC が増えます。

関係を持つ SFC の数が増えてしまうと言うことは、それだけ意識しないといけない SFC が増えてしまいます。 意識しないといけないものが増えるので、読むときの精神的な負担が高いコードになってしまいます。

# 2. 副作用がある。

第二の要因については props で渡してくれれば、関数として取り扱うことができ副作用がありません。 しかし Vuex の state を使ってしまうと、副作用を与えることができます。 Vuex は副作用、プロパティの変化を管理することを目的にしたプラグインです。

# ◯ 設定ファイル - config.js

もし単に Vuex がグローバル変数を管理するだけのものなら、 ここまでなるべく使うのは避けましょうね、とは言われないかなと思います。 例えば、サーバのアドレスなどを記載した「設定」は config.js などにまとめてグローバル変数として管理されているのをよく目にします。

config.js に書かれた内容も全ての SFC と関係を持ってしまうと言えます。 しかし Vuex のように設定をグローバルに書くのは、止めようという話は聞きません。 なぜでしょうか?それは「設定」は、副作用を持たないからだと思います。

config.js も書き換えれば変化します。 しかし、書き換える頻度は Vuex の state に比べると格段に少ないです。 即ち、意識しないといけない機会がそんなに多くなく、Vuex に比べれば負担が少ないからだと思っています。

副作用がなぜいけないかは、以下の記事で書かせていただきました。

# Vuex を使っても良いもの

とは言え、Vuex, グローバル変数を全く使わないと、辛い感じもします。 props と emit は、書くときも負担ですが、読むときも負担です。 Vuex を使うことのデメリットは、ワーキングメモリへの負担を強いることです。 どのようなケースなら、このデメリットを受け入れつつ Vuex を使っても、良いのでしょうか?

「変数のスコープは狭いほど良い」と妄信する - 分裂勘違い君劇場 by ふろむだ

変数でもメソッド名でもクラス名でも言えることだが、単純に「スコープは狭いほどよい」という方針でプログラムすると、逆に保守性も可読性も悪いプログラムができあがることがけっこうある*1。

実際、「あちこちから頻繁にアクセスするようなオブジェクトやメソッド」は、スコープをぐっと広くしてしまった方が(場合によってはグローバル変数やグローバル関数にしてしまった方が)、いちいちパラメータ渡しのバケツリレーをせずに、オブジェクトや機能を使うことができ、プログラムの可読性も保守性もずっと向上することがけっこうある。

# 対象 - データベース

データベースに副作用を与える操作は、Vuex を使っても良い気がします。 なぜなら、データベースに格納された状態は、どこからでも参照、変更できるグローバル変数だからです。 そのため、副作用を持ったグローバル変数を扱う Vuex を使った方が、 自然な書き方になることがあるあときがある気がします。

Vuexで扱うデータのほとんどがAPI通信を介して取得されるので、 非同期通信に関わる広範囲の領域の処理がActionに集約されていました。
Vue.js+Vuex+TypeScriptのWebフロントエンド開発現場を前向きに改善した話

Vue <-> Vuex <-> データベース

それでもデータベースを操作する処理だからと言って、 Vuex の state と関係のない処理まで Vuex に突っ込んでしまうと、 Vuex が肥大化してしまうので Vuex の state と関係の無い処理は避けましょうね、ということらしいです。

当然ですが、ActionからはVuexだ状態管理に関連のない処理は排除しました。 Stateの整理をしたことによりVuexで扱わなくなったデータの取得処理は、Actionではなくコンポーネントのメソッドに移動しました。 値を加工するだけの純粋関数等、AdapterやRepositoryに置き場所のない処理はhelperファイルに退避させました。
Webフロントエンド開発現場を前向きに改善した話

# 場所 - 「Atom」,「Molcule」レベルのコンポーネントの中では使わない

小さいレベルのコンポーネントからは、Vuex を参照、操作しないほうが良さそうです。 上は ZOZO に所属されている方による記事です。 下は BCcloud に所属されている方の記事です。

「Atom」「Molecule」レベルの汎用コンポーネントは、Vuex Stateを参照しないというルールです。
Webフロントエンド開発現場を前向きに改善した話

PageからのみStoreにアクセスする。
Speaker Deck - Nuxt.jsプロジェクトの改善テクニック

このようにして、小さいコンポーネントでは Vuex を使わない理由は2つあるかなと思っています。 まず第一に、1画面の中で複数の箇所から Vuex の Store への参照と変更が入り組んだ場合、処理が追いにくくなります。 また第二に、恐らく Vuex を使ってしまうと、他の Organism レベルのコンポーネントで使い回すことができなくなってしまうからだと思っています。 使い回しについては、以下で書かせていただきました。

# ◯ 気になること

ここで気になることは ZOZO の方と BCcloud の方で、若干意見が分かれていることです。 ZOZO の方は Organism レベル以上のコンポーネントからの Store の参照、変更を許しています。 対して BCcloud の方は、Organism レベルからの Store の参照を禁止していて、 Page レベルのみに限定されています。

資料を読むと BCcloud の方も最初は ZOZO の方と同じように Organism レベル以上のコンポーネントからなら Store の参照、変更をしても良いルールにしていたけど、 それでも苦しくなり、結局 Page レベルのコンポーネントに限定したそうです。 資料しか見ていないのでちゃんとしたことは一切わからないのですが、 恐らく BCcloud の方のほうが、ZOZO の方よりも、扱う処理が複雑だったのかなと、思っています。

Vuex を使って苦しくなる、処理が追えなくなる場合は、   1つの画面の中で Vuex に関連する処理、即ち dispath, commit や getters が、 入り組んで実行されるとき   だと個人的に考えています。 反面、Vuex を使っても苦しくならないのは、そこまで問題にならない場合は、   画面を跨いで Vuex に関連する処理、即ち dispath, commit や getters が、 入り組んで実行されるとき   だと個人的に考えています。

1つの画面の中で Vuex の操作が入り組んでしまう場合は、Vuex を使うコンポーネントのレベルを1つ上げ、 Vuex が1画面の中で複雑に作用しないように、せざる得なくなるような気がします。 途中で変更するのはかなり面倒なので、事前にある程度設計するなりすることが大切なのかなとは思うのですが、 将来を予測して設計するというのは、なかなか難しいのかなと思ったりもします。

# getters と actions と mutations

わざわざ actions, mutations や getters を使うのは面倒な気もします。 直接 state を変更したり参照したりしたいのですが、あまりそう言うのは見かけません。 なぜでしょうか?

# 1. getters の場合

大抵の場合、getters は書きましょうと書かれています。 特に getters で何か処理をしない場合にもです。 KISS の原則に則り、なるべく使わない機能は使わない、導入しないようにする、 というのが個人的な信条だったので疑問でした。 しかし、大方それで意見が一致しています。なぜでしょうか?

個人的におそらくフックする余地を残しておきたいからだと思います。 あとからちょっとだけ state の参照の仕方を、変えたいとなったとします。 その場合、直接 state を参照してしまっていると、 全ての箇所を書き換えないといけなくなります。 そういったことを避けたいのかな、と個人的に思っています。

# 2. mutations の場合

mutations についても、直接変更しないということで意見が一致しています。 特に mutations で何も操作しない場合にもです。これも疑問でした笑

これにはおそらく2つの理由があるかなと思います。 まず第一に getters と同じようにフックする余地を残しておきたいのだと思います。 また第二に ツール Vue.js devtools で「いつ」、「どこで」、「だれが」 state を変更を監視できなくなるからだと思います。

# 3. actions の場合

では「非同期処理」がなければ action を使わないで、 直接 mutation を呼び出して良いのか?と言う疑問があります。 以下の資料によると "個人的には呼び出してもいいと思っているが、 プロジェクトとしては呼び出しを禁止することは多い" とのことです。

特に actions で何も操作しない場合にも dispatch を呼び出すメリットとデメリットについて考えます。

メリットは、書く時は、フックする余地を残すことができます。 actions で書こうかな、mutations で書こうかな、と考えて精神的に疲弊することがなくなります。 読むときのメリットは、特にないかなと思います。

デメリットは、書く時は、面倒です。 読む時は、ほんの少し負担が増える気がします。 mutations に比べて actions はできることが多いので、 コードを初見したときにこんなことしてるかもな、あんなことしてるかもなと、 いくつか候補が上がってきて若干疲れます。

# ◯ 「一方向」あるいは「Flux」

「一方向」あるいは「Flux」とかいう謎の用語が、 たまに登場します(CV: ぴよぴーよ速報)。 そして、この  「一方向」  あるいは  「Flux」  は、  処理に名前をつけるため  に存在します。

React の Redux は、 Vue でいうところの Vuex です。 Redux は Flux の考え方をもとに実装されているらしいです。 Redux のアプローチは、以下のようなものです。

  1. まず、変更するオブジェクトを store から取り出します。
  2. 次に、取り出したオブジェクトのコピーを作ります。
  3. コピーに対して変更します。
  4. そして、変更したコピーを store に保存します。
  5. 保存されたコピーを参照します。

Step 1 で、わざわざコピーを作成しているのは、 変更 mutation の前後を確認しやすいようにしたいからだと思います。 もしコピーを作成しなかった場合、1つずつプロパティを変更することになります。 そうすると、Step 1 ~ 4 の処理、変更 mutation に対して名前をつけられなくなります。

いま、開発ツールのミューテーションのログを見ながら、 アプリケーションのデバッグを行っていることを想像してください。 全てのミューテーションをログに記録するためには、 ミューテーションの前後の状態のスナップショットを捕捉することが必要です。
ミューテーションは同期的でなければならない - Vuex

この一連の流れに対して、正直「一方向」とか Flux なんてかっこいい名前ではなく、 「取り出して、コピって、変更して、保存する、そして最後に参照する。」っていって欲しかったです。 Vuex の資料でもこの「一方向」という謎の用語がたまに出てくるので、 心の片隅にあってもいいかなと思いました。

Vuex は、この Flux の考え方を崩して、コピったりしなくても、 とりあえず「変更 mutation に対して名前がついてたらいいよね」、 という感じになっているのだと、個人的に思っています。  関数を使えば、処理に名前をつけることができます 

関数の最も基本的な機能は、複数の命令をひとつにまとめて、名前をつけることです。
JavaScriptの関数で何ができるのか、もう一度考える - Subterranean Flower Blog

もはやこうなってくると単なるセッター, ゲッターでしかないですから、 Vuex は、一方向とか、あまり関係ないのかなと思うのですが、 なぜか「一方向」という謎の用語が使われます。 データの流れが、一方向と言えば、一方向ですが、最初は特別な何かなのかなと思い結構混乱しました笑

# ◯ actions と mutations の違い - その1

actions と mutations の引数を見ると、若干違うことに気づきます。 actions の context の中には dispatch, commit があり、ほかの actions, mutations を実行することができます。 対して mutations は、state しか渡されず、actions, mutations を実行することができません。 なぜでしょうか? おそらくこれは、変更を追跡する処理の単位として活用したいのかなと思いました。

export default {
  actions: {
    // actions には commit や dispatch が渡されて
    // ほかの actions, mutations を実行できる。
    signin ({commit, dispatch}, userForm) {
      user = axios.post('http://example.com', userForm).data
      commit('setUser', user)
    }
  },
  mutations: {
    // mutations には state しかない。
    // ほかの処理は実行できない。
    // => mutations は、監視する処理の最小の単位を定義する。
    setUser (state, user) {
      state.user = user
    }
  }
}

これらの変更は非同期に発生します。1つの変更は、複数の他の変更を引き起こす可能性があります。 ピンポン球が入った袋すべてをあなたがプレイしている最中に投げ込み、 いろいろな場所に飛んでいろいろな方向に飛んでいくことを想像してみてください。
漫画で説明する Flux

# ◯ actions と mutations の違い - その2

非同期処理は、actions に書きます。mutations に書けません。 actions と mutations が分かれている理由は、 ツールで変更を追跡できるようにするためのようです。

ツールで追跡するためには、非同期処理があると、ツールで変更を追跡できなくなるからだそうです。 どういうことかというと、「ある関数」の中に「非同期処理」がはいっていると、 開始した時はわかりますが、  終了した時がわかりません。  「ある関数」が終了しても、その中で実行した「非同期処理」は、まだ実行されていて state を変更するかもしれません。 以下、Vuex の公式ドキュメントです。

Vuex が非同期処理を await で止めてくれるような実装にしてくれたら、とも思ったのですが、 非同期処理には、await で止められる async だけではなく setTimeout 関数や Promise による then チェーンをしている場合もあります。 これらは await で止めることができません。 mutations には、素直に同期処理だけ書くより他になさそうです。

# Vuex が難しい

Vuex の取り扱いが分からなさ過ぎて辛いです。

Vuex の取り扱いについてだけで、たくさんの発表がなされています。 上記に記述した以外のものを、以下に追記します。 難しくて、ほとんど理解できていません。

なんで自分が Vuex について悩んでいるんだろうと思ったときに、  スコープ  について、悩んでいるのだと、個人的に思っています。 DOM という木構造に深くネストしたスコープの中で、 どのようにして名前解決を実現するのか、ということをいま現在も試行錯誤が繰り返されています。

しかも、この DOM と言う木構造の中でなされる名前解決のなされ方が、 静的名前解決ではなく動的名前解決のような状態になっているため、 この世の地獄の地獄みたいなことになっています(CV: ぴよぴーよ速報)

// 静的名前解決
// スタティックスコープあるいはレキシカルスコープ
//   関数を定義したスコープで名前が解決される。
function f() {
  const x = 1  // <--- こっちが参照される。
  function g() {
    return 2 * x
  }
  return g
}
const x = 0
g_ = f()
g_() === 2
// true
// 動的名前解決
// ダイナミックスコープ
//   関数を実行したスコープで名前が解決される。
//   近年のプログラミング言語では採用されていないそうです(Wikipedia 調べ)。
function f() {
  const x = 1
  function g() {
    return 2 * x
  }
  return g
}
const x = 0 // <--- こっちが参照される。
g_ = f()
g_() === 0
// true

そもそもフロント側ではあまり何もしない方が良いというツイートも見かけました。 大変勉強になりました。ありがとうございます。

ここで記述されている「頭の良いコード」とは、おそらくビジネスロジックのことを指しているのかな、と思いました。 DOM あるいは Single File Component といった地獄の地獄の中とでなはなくとも、 例えば、通常の JavaScript ファイルの中にビジネスロジックを書き込むことも考えられますが、その場合は、どうでしょうか?

しかし、そうであったとしても、フロントエンド側で実行した処理をもとに、バックエンド側で「更新」する作業は必要になります。 「更新」の実装はとても面倒です。

CRUDのうちUPDATEがもっともシステムを複雑化する。更新には複雑なルールが伴うからだ。
イミュータブルデータモデル - scrao.io

モデルの複雑性を増すのは、CRUDのうち のUPDATEに関する要件です。 モデルに対するデータの更新を極限まで 削ることによって、拡張に対して開いて いて、修正に対して閉じている堅牢なも のにします。
イミュータブルデータモデル(入門編)

「更新」が、難しいとはどういうことでしょうか? 適切な例ではないとは思うのですが、エアコンのリモコンについて考えます。 自分の家のエアコンは、ボロいので、リモコンを操作しても本体が反応しないことがあります。 その時はリモコンで表示されている設定と本体の設定が乖離してしまうことになります。 これを差分を「更新」によって、実装することを考えると絶望します。 「削除」して「作成」する方が圧倒的に楽です。 リモコンから「2度上げました」という差分を「更新」する実装を考えたり書いたりするよりも、 リモコンから「28 度」というデータをそのまま送ってもらって、現在のエアコンの本体の設定を「削除」して「作成」する方が楽です。

React はその更新されたデータを使ってページ上の UI を 1 から作り直します。
【後編】Reactとその誕生の背景を乱暴に解説してみた - YouTube

万が一、ビジネスロジック的なものを実装しなければいけない場合も、 我が家のボロいエアコンのリモコンと同じようにして、差分の「更新」ではなく、 設定を「削除」して「作成」するような、データをガツンとぶち込むような、 イミュータブルなデータモデルを設計をしておいたほうが良いのかなと思いました。

# Vuex 以外のアプローチ

Vuex 以外のアプローチを2つ見つけました。 面白かったので、以下に追記します。

# 1. Vue.observable

1つめ、 Vue.observable で状態管理

# 2. Provider

2つめ、 終わりゆく Vue 2.x 時代の状態設計のアンサー - Vue 3 の Provider への期待

# おわりに

以上になります。ありがとうございました。