定義
検索窓のようにユーザーがタイプするたびにサーバーへリクエストを送る画面で、「画面に最終的に残る結果が常に最後に打った検索語の結果」になるよう保証したいときに使うパターンである。速くタイプするとリクエストが複数重なるが、誤って以前の検索語の結果が画面に残る事故を防ぐ。
3つの道具を組み合わせる: debounce(入力がしばらく止まるまで待ってから一度だけリクエスト — 例: 200ms以内にまた打てばリクエストを先送り)で呼び出し回数を減らし、AbortController(進行中のfetchリクエストを途中でキャンセルするブラウザ標準オブジェクト)で以前のリクエストをキャンセルし、だんだん大きくなる「最新リクエストid」を置いて遅れて到着した応答(out-of-order — 先に送ったリクエストがより遅く到着すること)を無視する。
なぜ必要か
速くタイプすると複数のリクエストが同時に飛び、ネットワーク遅延のせいで先に送ったリクエストがより遅く到着しうる。そのまま画面に描くと、古い結果が最新の結果を上書きする競合条件(race condition — 複数の作業の完了順序が絡み合い、結果がずれる状況)が生じる。また、キー入力ごとにリクエストするとサーバー負荷も大きくなる。このパターンは呼び出し頻度・キャンセル・順序保証を一緒に扱い、常に最新のクエリだけを反映する一貫したUIを作る。
動作原理
- 入力変更 → debounceタイマー設定、以前のタイマーはclear。
- リクエスト直前に
id = ++refを付与、応答到着時にid === refのときだけstateに反映。 - 新規リクエスト/アンマウント時にAbortControllerで以前のin-flightリクエストをabort。
- 結果・状態はrender中に派生: 最後にresolvedしたクエリ == 現在のクエリかでstatus(idle/loading/ready/error)を計算し、状態の書き込みは非同期コールバックでのみ行う。
実務適用
トレードオフ
debounceウィンドウが長いほどリクエストは減るが反応が遅くなり、短いと反応は速いがリクエストが増える。idガードだけでも整合性は保てるが、AbortControllerまで使えば不要なネットワーク・サーバーコストをさらに減らせる(キャンセル対応が必要)。
使ってはいけない場合
単発・冪等・順序無関係のリクエストや、結果が累積されるべき場合(無限スクロールのappendなど)には、「最新だけ反映」ルールがかえってデータを捨てる。こういうときは累積マージ戦略を使う。
よくある間違い
- abortエラーを一般エラーとして処理し、ユーザーにエラーUIを露出する。
- リクエストidをstateに置いて不要なレンダーを誘発する(refが正しい)。
- effect本体で同期setStateでresultsを書き、重複レンダー・ちらつきが発生する(render中の派生が安全)。
関連概念
- serverless-stateless-execution — これらのリクエストが到達するサーバー側検索関数の実行モデル。