Redux の基本的な使い方は、グローバルストア内の Slice を action 経由で更新することです。一元化した変更メッセージが Selector 経由で効率的に UI コンポーネントに流れます。
Slice 更新しない処理や、非同期処理などこの原則にフィットしないフロー一般については、
createListenerMiddlewareを用いた Listener で取り扱えます。
ストア変更以外が主題となるため、 UI 開発から見ると ListenerMiddleware を導入する直接の必要はありません。ただ、Redux の.dispatch()による統一的なフレームワークにのるため、既存 action を Reducer に加えて Linstener のeffectでも扱うことで、非同期副作用を追加できます。
また、Listener にしか流れない action も追加でき、 DevTool で把握できます。カスタム middleware を追加すると、テストケースでイベントトレースを assert する方法もとれます。
Promise による API 共通ロック
複数の API コール完了を待ちたいときには、次のような Promise ガードパターンを併用できます。
Promise は処理中の await はよく知られているとおりの挙動で、参照を保持しておけば処理が完了したあとも目的の値を返せます。
let promise = null; // ESM 内グローバル変数
export async function api_guard() {
if (!promise) {
store.dispatch(apiCallStarted());
promise = async_executor();
}
return promise;
}
この部分は Redux とは無関係のカスタム実装になります。Listener は await する対象を提供していません。
store.dispatch(apiCallStarted())の行は必要に応じて Listener に任意の action を送る処理の例です。この関数を呼び出す関数が複数存在する場合にもasync_executor()は一度だけ実行され、その直前にstore.dispatch()します。
この開始に対応するstore.dispatch(apiCallCompleted())を追加したい場合には、クライアント本体であるasync_executor()末尾に実装します。
API バスの意義
このようなメッセージバスを追加実装する意義は、アプリケーションのライフサイクルが UI コンポーネントよりも長いことから来ています。
API はアプリケーションのステートであるため、UI 実装のファイルと独立で実装する必要性が強いと言えます。
ページ遷移を持つアプリケーションについて考えれば自明でしょう。
データストアそのものについては Redux の基本機能でカバーできます。
そのデータをセットアップするプロセスは RTK Query を活用するか、適合しづらいケースではこのようなカスタム実装が必要になるはずです。