ローディング状態はドメインか?通信状態とドメイン状態の見分け方
TL;DR
loading/pendingのような取得中フラグは基本的にドメインではなく通信・アプリケーション層の関心事。- 見分け方は「その状態が業務ルール(ユビキタス言語)に登場するか」。「審査中」はドメイン、「fetch中」は違う。
- 迷ったら「バックエンドを同期APIに置き換えても残る状態か?」と問う。残るならドメイン、消えるなら通信状態。
- 構造としては RemoteData(外側・通信状態)がドメイン型(内側)を包む。ドメインモデルに
isLoadingを混ぜない。 - TanStack Query 利用時は query 層が
status/fetchStatusとしてすでに所有しているので、自前の RemoteData 型はほぼ不要。
このドキュメントの射程
- フロントエンドで「ローディング中かどうか」のような状態をドメインモデルに含めるべきか迷ったときの判断基準。
- 同じ boolean / 直和型でも、置き場所(ドメイン層 vs UI・通信層)が変わるケースの整理。
原因(なぜ混同が起きるか)
- どちらも「状態」であり、discriminated union で表現すると見た目が同じになるため。
isLoading: booleanをドメインモデルに直接生やすと、「items が空なのはロード中だから?本当に空?」のような不正状態が表現可能になる。RemoteData パターンの原典(Kris Jenkins, 2016)が指摘したのはまさにこの問題で、isInProgress: trueかつerrorあり かつdataあり、のような矛盾した組み合わせが型上作れてしまう。- Jenkins 自身の定式化は「REST リクエストには 4 状態ある(NotAsked / Loading / Failure / Success)」であり、これはリクエストのモデルであってデータそのもののモデルではない。ここを読み飛ばすと「状態=ドメイン」と混同する。
解決(判断基準と構造)
判断基準
| 問い | Yes → ドメイン | No → 通信/UI層 |
|---|---|---|
| ドメインエキスパートとの会話に出てくるか | 「審査中は編集不可」 | 「fetch中」は出ない |
| 業務ルール・不変条件がその状態に依存するか | 「承認済みのみ発送可」 | loading は何も規定しない |
| バックエンドを同期APIにしても残るか | 「下書き→提出→承認」は残る | loading は消える |
構造:包む、混ぜない
// ❌ ドメインモデルに通信状態が漏れている
type Order = {
id: OrderId;
isLoading: boolean; // 注文自体に「ロード中」という業務的意味はない
items: OrderItem[];
};
// ✅ 通信状態(外側)がドメイン型(内側)を包む
type RemoteData<E, A> =
| { type: "idle" } // NotAsked:未取得と取得済みを区別する要
| { type: "loading" }
| { type: "failure"; error: E }
| { type: "success"; data: A };
type OrderQuery = RemoteData<ApiError, Order>;
// ✅ 業務上のライフサイクルはドメインとしてモデル化する(通信とは無関係に存在)
type Application =
| { status: "draft"; content: DraftContent }
| { status: "underReview"; submittedAt: Date }
| { status: "approved"; approvedAt: Date }
| { status: "rejected"; reason: RejectionReason };
successのときだけ完全なOrderが存在することが型で保証される。「parse, don't validate」と同じく、境界を越えたら通信の事情を持ち込まない。NotAsked(idle)の存在がパターンの核心。isLoading: falseだけでは「未取得」と「取得済み」を区別できない。
2026年時点の実務的な着地点
- TanStack Query を使うなら、通信状態は query 層が
status/fetchStatusの直積として所有済み。自前で RemoteData を定義してドメイン型に混ぜる必要はない。 - コミュニティの語彙では「server state vs client state」として定式化されており、「API レスポンスを Redux/Zustand に入れて loading/error フラグを手動管理する」のがアンチパターンとされる。
- 逆方向の注意:
loggedInやisPaidMemberのようなバックエンド由来の値をそのままコンポーネントで使うのは業務知識の漏れ出し(フロントエンドのドメインモデル貧血症)。同じ boolean でも「有料会員かどうか」はドメインなのでモデル化する。業務ルールに登場するかで置き場所が変わる。
まとめ
- ローディング状態は「リクエストのモデル」、業務ステータスは「ドメインのモデル」。RemoteData で外から包み、ドメインモデルには混ぜない。
参考
- Kris Jenkins, "How Elm Slays a UI Antipattern" (2016): https://blog.jenkster.com/2016/06/how-elm-slays-a-ui-antipattern/
- krisajenkins/remotedata (Elm package): WebData でドメイン型を包む使い方
- Joan Llenas, "RemoteData, slaying a UI Antipattern with Angular": https://dev.to/joanllenas/remotedata-slaying-a-ui-antipattern-with-angular-2i2h
- TanStack Query docs, "Does TanStack Query replace Redux, MobX or other global state managers?": https://tanstack.com/query/v5/docs/framework/react/guides/does-this-replace-client-state
- chot Inc., 「フロントエンドもドメインモデル貧血症にしない」: https://zenn.dev/chot/articles/2ceaaf06ee56d9
- little hands' lab, 「簡単にできるDDDのモデリング」(業務ステータスはユビキタス言語側にある傍証): https://little-hands.hatenablog.com/entry/2022/06/01/ddd-modeling