Redux を仕事と個人で使い始めて 3・4 週間という経験値なのですが、「唯一の状態である state の唯一の更新手段である Action 発行」が、更新以外の作用を伴うことに頭を悩ませていて、この点についてのお話をさせてもらいたいと思います。
なお、前提とする Redux ミドルウェアや周辺ライブラリは、公式推奨である以下です。
Action 発行に伴う 3 つの作用
Redux の Action 発行(= dispatch の実行)は、以下の 3 つの作用をセットで実行する処理だと、自分は考えています。
それらの作用は常に全てが必要ではない
それぞれは勿論必要な処理ですが、常にセットで必要とは限りません。
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
-
Redux 実装上の制約があるわけではありませんが、Reducers に "We don't mutate the state" 以下の様に記載されているので、必須の処理だと考えられます。 ↩
-
Usage with React や Examples (特に React を使わない counter-vanilla)から、dispatch 時に render するのが意図した設計だろうと考えました。 ↩
-
最大で Reducer で分割された範囲内だけのコピーだとしても、です。 ↩
-
当記事公開から 2 週間実装を積み重ねた今の感想ですが、この通りに Reducer は単なる setter でありその処理や判断はしない、という設計がしっくり来るような気がしています。(※個人の感想です) ↩
-
redux-saga などの Action を別用途に使うミドルウェアもありますが、そこでオレオレモジュールを呼び出すだけなのであんまり変わらなさそうです。 ↩
-
趣旨から外れるので書きませんが、「初期状態の定義場所が一箇所に明確に限定される」など良いと感じた点もあります。 ↩