複数の作用がセットのAction発行とどう向き合うか

  • 10
    いいね
  • 5
    コメント

Redux を仕事と個人で使い始めて 3・4 週間という経験値なのですが、「唯一の状態である state の唯一の更新手段である Action 発行」が、更新以外の作用を伴うことに頭を悩ませていて、この点についてのお話をさせてもらいたいと思います。

なお、前提とする Redux ミドルウェアや周辺ライブラリは、公式推奨である以下です。

Action 発行に伴う 3 つの作用

Redux の Action 発行(= dispatch の実行)は、以下の 3 つの作用をセットで実行する処理だと、自分は考えています。

  1. state の更新をする。
  2. state のコピーをする。1
  3. 描画を実行する。つまり React の render 関数を実行する。2

それらの作用は常に全てが必要ではない

それぞれは勿論必要な処理ですが、常にセットで必要とは限りません。

3 が困る場合

単純に、描画に関係のない状態の更新であれば、実行をしない選択が出来る方が有り難いです。

React 側の「同期的な描画関数の実行は一回にまとめてくれる機能」を前提としても、例えば「60 fps でゲームプレイ時間を更新する」という処理がある場合、各フレームの処理は非同期であり、それぞれで描画が実行されてしまいます。

その場合は、「表示時間は秒単位だから、秒が繰り上がるときだけ描画する」ような調整をしたいです。

2 が困る場合

描画エンジン側が state に対して持つ参照の期間が不明な点を考慮するなら、一般的なプログラミングのルールとしては適切だと思います。

ただ一方で、巨大なオブジェクトや配列がそこに含まれる可能性もあり3、その際に、state の更新に対して都度変数のコピー処理が実行される仕組みは、規模の想定ミスやコーディングのイージーミスから大きな問題につながることが予測され、この点は良いことではありません。

これが Redux 内で必須の作法だとされているのは、3 のために都度描画関数に投げるためだと思ってますが、そもそもその 3 が要らないのであれば両方不要になります。

なお、1 だけを実行したい場合とは

ということで、もし 1 の state の更新を行いたいだけの場合も、2・3 の作用も発生してしまいます。

state の更新のみをしたい場合は数多くあり、先程のタイマーの例のように内部状態の変更が出力の変更にならない状況や、その他、時間の掛かる処理(Web-API アクセスなど)の結果を予め実行して保持して置きたい場合などがあります。

現在どんな Action / Reducer 設計にしているか

そのような Action の仕組みを受けて、個人的に今どういう設計にしているかです。

Reducer はいわゆる「リソース」別に定義しているとは思いますが、基本的にそのスコープが許す全体を何も考えずに更新する Action を一つ定義して、ほぼそれだけしか定義しません。
例えば、配列なら UPDATE_FOOS 、オブジェクトなら EXTEND_FOO のような感じです。

これは、Action の実行回数を減らすため、もしくは書き直しの手間を省くためです。
わかり難いので例を書くと、例えば、配列のある 1 行を更新する UPDATE_ROW を定義してしまったとして、その後もし複数行の更新が必要な Action が必要になった場合に、前述の理由で UPDATE_ROW をループして dispatch する処理は不適切なことが多いです。
その際に UPDATE_ROWS を別に作るくらいなら、最初から UPDATE_ROWS を定義してしまえ!という判断でした。

必定、その Action が取りうる引数の範囲は広くなるため、state を守るためのバリデーション処理や変換処理などが別途大量に必要になりますが、それは適当にオレオレモジュールを作り、Action Creator 内で呼び出して全てを適切に処理します。

Reducer 側で何もしないので、処理の本体はそのオレオレモジュール群の集合となり、一体 Redux とは何だったのか・・・4という有様です。5

加えて、そのオレオレモジュール群ですが、state がプレーンオブジェクトであるべきという制約上、クラスインスタンスで状態の保持ができないので、「第一引数に、ある決まったスキーマのプレーンオブジェクトを要求して計算して返す」ような表現の関数だらけになります。
JSON スキーマ大好きな人は幸せですが、自分はそうではないので単なる縛りプレイです。

以上

自分の Redux の理解がどこか間違ってるといいなって思って書きました。
色々ミドルウェアを使う前に生 Redux の力を知りたいです。6



  1. Redux 実装上の制約があるわけではありませんが、Reducers に "We don't mutate the state" 以下の様に記載されているので、必須の処理だと考えられます。 

  2. Usage with ReactExamples (特に React を使わない counter-vanilla)から、dispatch 時に render するのが意図した設計だろうと考えました。 

  3. 最大で Reducer で分割された範囲内だけのコピーだとしても、です。 

  4. 当記事公開から 2 週間実装を積み重ねた今の感想ですが、この通りに Reducer は単なる setter でありその処理や判断はしない、という設計がしっくり来るような気がしています。(※個人の感想です) 

  5. redux-saga などの Action を別用途に使うミドルウェアもありますが、そこでオレオレモジュールを呼び出すだけなのであんまり変わらなさそうです。 

  6. 趣旨から外れるので書きませんが、「初期状態の定義場所が一箇所に明確に限定される」など良いと感じた点もあります。 

この投稿は Redux Advent Calendar 201619日目の記事です。