Runner in the High

技術のことをかくこころみ

フロントエンドアプリケーションにおいて状態をどこに置くべきか論

後学のために自分の考えていることをまとめてみる。

考えられるパターン

これまでの経験から以下4つのパターンがある。

  1. ローカルStateでprop-drillingする
  2. ローカルStateかつイベント経由でデータ交換をする
  3. グローバルStoreとローカルStateを併用する
  4. グローバルStoreのみを使用する

1. ローカルStateでprop-drillingする

propとしてコンポーネント間のデータをやりとりする手法。

ほぼすべてのUIコンポーネントを親からデータを受け取りDOMを出力するだけの純粋な関数として表現できるため、全体の設計自体はシンプルになる。手間は多いが魔法は少ない。

コンポーネントの粒度が小さいアプリケーションの場合にはいわゆるバケツリレーと揶揄されるデータの受け渡しが頻発し、これに嫌悪感を持つエンジニアもいる。

2. ローカルStateかつイベント経由でデータ交換をする

Flux登場以前のフロントエンド・アプリケーションでよく見られた手法。

ほとんどFluxライブラリの再発明と同じであるが、コンポーネント間で双方向にデータが飛び交う点が大きな違い。イベントをインターフェイスとしてコンポーネントの関係性を疎結合にできる点がメリット。このデータの流れを単方向にしたものがFluxアーキテクチャである。

Vue.jsやAngular.jsにおいてはかつて親子間でのイベント送信を相互に行える機能が用意されていたため、これを使って横断的にコンポーネント間通信をする設計で開発が行われ徐々にカオスになっていく事例が見られた。いまはFluxパターンの登場によってアンチパターンになったと思われる。

3. グローバルStoreとローカルStateを併用する

仕様に応じてグローバルStoreとローカルStateを使い分ける。

一見良さそうなアイデアに見えるが、この手法の大きな問題点は「なにをStoreに置き、なにをStateに置くか」のルールづくりが大変なこと。チームが大きくなるとすぐにルールが守られなくなる。また、UIの改修によって状態の置き場がStateからStoreに移動したりなど、仕様変更時の影響範囲が状態管理部分を巻き込んで大きくなる可能性もある。

一方で、ローカルStateにすることで影響範囲を小さくしたり、別コンポーネントからの意図しないデータ参照を防ぐなどの設計も可能になるのがメリット。しかし、これはグローバルStoreにデータを置いたうえで仕様に応じた個別のデータ型を用意することでも達成できる。

4. グローバルStoreのみを使用する

グローバルStoreのみを状態管理に使う手法。

この手法では「グローバルStoreが膨らんで困る」とよく言われる。ココで言うところの「困る」とは大抵の場合「グローバルStoreのフィールドが増えて見づらくなる」と「グローバルStoreが大きくなると状態更新差分パフォーマンスが悪くなる」のふたつの懸念に集約される。これがクリアできるのであれば有用な手法になる。

利用するFluxライブラリによってはこれを解決できる機能や追加のプラグインなどが用意されていることがある。

どれを使うべきか。

まず、チームメンバが1人とか2人とかの場合にはどれを使おうが気にする必要はない場合が多い。好きなのを使えばよい。よっぽどめちゃくちゃな人がいなければコードベースは破綻しない。

しかし、経験上4-5人目くらいからが分岐点で、更に「2-3人のメンバで構成されるチームが複数ある」のようなケースになると途端に画一的なルール作りのほうが重要になってくる。設計方針に秩序を持たせたい場合には「いい感じでよしなにお願いします」がワークしなくなる。

ルールの作りやすさ順でいうと1>4>3>2かなという感じ。

Elmの場合には1しか選択肢がないのであんまり悩むことがない。