遅れて届いた応答を退ける検索hook
目標
React+TypeScriptの検索hookを自分で実装し、古いレスポンス、キャンセル、再試行、例外を検査します。 提供されたUIで、実際のReact画面を見ながら練習します。HTMLフォームの作成と長い一覧の最適化のすべてを、このラボ1つで代えるものではありません。
なぜ重要なのか
成功したレスポンスでも、現在のユーザーの意図と違えば、間違った画面です。キャンセルはリソースの回収であり、結果の無視は画面の正確さです。 2つの責任を分ければ、キャンセルに対応していない非同期ツールもテストできます。Promiseの拒否と、呼び出し時のthrowも、別の経路です。 関数・クロージャ・Promise・TypeScriptのユニオンと、基本的なReact hookが読める必要があります。
準備
最初に1回、node /opt/react-workbench/prepare.mjs /root/react-search-lab lessonを実行してください。
すでに存在する場合は、既存のコピーを使い続けてください。削除したり上書きしたりしません。ランタイムのnpm installは不要です。
編集するファイルは/root/react-search-lab/src/useSearch.tsです。一緒に提供されているsrc/types.tsで、
Query(text, revision)、Search(query, signal)、SearchStateの5つの状態の契約を読んでください。
src/types.tsと、提供されているsearchの動作は変更しません。ステップの例は最初の枠組みであり、正解ではありません。
ステップ
- 検索を送る前と待っているときを分ける: lessonモードで/root/react-search-labのコピーを準備してください。src/useSearch.tsにuseSearch(request, search)をexportし、SearchStateを返します。空のrequest.textはidle、空でなければloadingと該当するqueryです。入力を空にして新しく提出すると、idleに戻ります。レスポンスの処理は次のステップです。
- 成功・空の結果・失敗に別の案内を出す: 与えられたsearch(request.text, controller.signal)を呼び出してください。1件以上の結果はsuccessと元のitems、0件はempty、Errorによる拒否はerrorとerror.messageです。各状態に該当するqueryを保持し、失敗のあとの新しい検索は、loadingを経て成功できなければなりません。searchを独自の実装に置き換えないでください。
- 猫にクジラの画面を奪わせない: 複数の検索を提出し、レスポンスを逆順に完了させても、最新の提出の結果だけが残るようにしてください。最新のリクエストが失敗したあとに古い成功が届いても、最新のエラーを維持します。キャンセルを無視する配信でも成り立つ必要があります。
- 古い障害の案内も捨てる: 最新の成功のあとに古い失敗が来ても、最新の失敗のあとにさらに古い失敗が来ても、画面を書き換えられないようにしてください。最新のリクエストのError.messageは、そのまま保存します。前のステップの成功レスポンスの保護も維持します。
- 不要なリクエストを後始末する: 新しい提出でエフェクトが変わるとき、前のリクエストのsignal.abortedがtrueになるようにしてください。最新のリクエストは、必要な間はfalseで、コンポーネントの破棄時にはtrueである必要があります。キャンセルの有無と、遅れたレスポンスの無視を、それぞれ維持します。
- 同じ単語の新しい意図を区別する: Queryのrevisionが異なる同一の検索も、新しいリクエストでなければなりません。同じ単語の2つのリクエストを逆順に完了させると、新しい結果だけが残ります。空の提出はidleに戻り、前のレスポンスが結果を復元できないようにしてください。
- Promiseを受け取る前に起きたエラーを処理する: searchがPromiseを返す前にErrorをthrowしても、React画面全体が死なず、errorとそのmessageを表示するようにしてください。その後、新しい検索で正常に復旧できる必要があります。非同期の拒否・キャンセル・競合の契約も維持します。
- セットアップ・クリーンアップを繰り返し、全体の契約を再検査する: 開発用のStrictModeの、セットアップ→クリーンアップ→再セットアップで、最初のリクエストがキャンセルされ、最初のレスポンスが遅れて届いても2回目の結果が残るかを検査してください。全体の採点で、ステップ1–7とこの条件を一緒に確認します。npm run typecheckとnpm run buildのあと、ポート3000のプレビューで、逆順の成功・失敗・空の結果・再試行を自分で確認してください。
参考
作業フォルダーでnpm run typecheck、npm run build、npm startの順に実行すると、
http://127.0.0.1:3000/のプレビューを見られます。末尾の/は残してください。
ソースを変更したら、再ビルドしてプレビューを更新します。開始ステップでは、まだレスポンスが表示されないことがあります。
手動配信は、HTTPではなく決定的なPromiseのモデルです。実際のHTTP比較画面では、ラボサーバーの合成データを受け取ります。
採点は、学習者のhookのstrict型と、実際のReact+jsdomの状態を検査し、学習者のファイルを変更しません。
この検査は、実際のブラウザのレイアウト・パフォーマンス・スクリーンリーダーの使いやすさの検証の代わりにはなりません。
各ステップは、前のステップも累積して検査します。128KiB以下の一般的なソースファイル、作業プロセスの12秒制限を適用します。
最後の回帰ステップは、前の正解でも通過できます。新しい機能ではなく、既存の契約を再検証するからです。
必要なら+時間でセッションを延長し、終了前に、編集したソースと観察記録を別に保存してください。セッション終了後、ファイルは保持されません。
検索を送る前と待っているときを分ける
lessonモードで/root/react-search-labのコピーを準備してください。src/useSearch.tsにuseSearch(request, search)をexportし、SearchStateを返します。空のrequest.textはidle、空でなければloadingと該当するqueryです。入力を空にして新しく提出すると、idleに戻ります。レスポンスの処理は次のステップです。
useStateの初期値と、useEffectで提出された検索を処理する仕事を分けて考えてみてください。検索ボックスに入力中のdraftは、直接読みません。
成功・空の結果・失敗に別の案内を出す
与えられたsearch(request.text, controller.signal)を呼び出してください。1件以上の結果はsuccessと元のitems、0件はempty、Errorによる拒否はerrorとerror.messageです。各状態に該当するqueryを保持し、失敗のあとの新しい検索は、loadingを経て成功できなければなりません。searchを独自の実装に置き換えないでください。
AbortControllerでsignalを作り、Promiseのthenとcatchをつないでください。emptyは障害ではなく、正常なレスポンスの一種です。
猫にクジラの画面を奪わせない
複数の検索を提出し、レスポンスを逆順に完了させても、最新の提出の結果だけが残るようにしてください。最新のリクエストが失敗したあとに古い成功が届いても、最新のエラーを維持します。キャンセルを無視する配信でも成り立つ必要があります。
各エフェクトのセットアップの中で作られた有効性の値を、成功のコールバックが確認するようにし、クリーンアップでその値だけを失効させてください。すべてのリクエストが共有するtrue/false1つは、再びオンになることがあります。
古い障害の案内も捨てる
最新の成功のあとに古い失敗が来ても、最新の失敗のあとにさらに古い失敗が来ても、画面を書き換えられないようにしてください。最新のリクエストのError.messageは、そのまま保存します。前のステップの成功レスポンスの保護も維持します。
thenだけを保護すると、catchは依然として画面を書き換えられます。2つの経路が、同じエフェクトの有効期間を読んでいるかを確認してください。
不要なリクエストを後始末する
新しい提出でエフェクトが変わるとき、前のリクエストのsignal.abortedがtrueになるようにしてください。最新のリクエストは、必要な間はfalseで、コンポーネントの破棄時にはtrueである必要があります。キャンセルの有無と、遅れたレスポンスの無視を、それぞれ維持します。
クリーンアップ関数の中で、このエフェクトが作ったcontrollerだけをキャンセルしてください。結果を無視するだけでは、外部の作業は止まりません。
同じ単語の新しい意図を区別する
Queryのrevisionが異なる同一の検索も、新しいリクエストでなければなりません。同じ単語の2つのリクエストを逆順に完了させると、新しい結果だけが残ります。空の提出はidleに戻り、前のレスポンスが結果を復元できないようにしてください。
依存値に検索文字列しかないと、同じ単語の再提出を見逃します。requestは提出ごとに新しいオブジェクトになるという、提供UIの契約を活用してください。
Promiseを受け取る前に起きたエラーを処理する
searchがPromiseを返す前にErrorをthrowしても、React画面全体が死なず、errorとそのmessageを表示するようにしてください。その後、新しい検索で正常に復旧できる必要があります。非同期の拒否・キャンセル・競合の契約も維持します。
search(...).catch(...)では、search自体のthrowはcatchに届きません。呼び出しをPromiseのコールバックの中に移すか、try/catchで2つの経路を一緒に扱ってみてください。
セットアップ・クリーンアップを繰り返し、全体の契約を再検査する
開発用のStrictModeの、セットアップ→クリーンアップ→再セットアップで、最初のリクエストがキャンセルされ、最初のレスポンスが遅れて届いても2回目の結果が残るかを検査してください。全体の採点で、ステップ1–7とこの条件を一緒に確認します。npm run typecheckとnpm run buildのあと、ポート3000のプレビューで、逆順の成功・失敗・空の結果・再試行を自分で確認してください。
最後のステップは、新しいフラグを足す課題ではなく、累積した実装の回帰検査です。開発用のStrictModeの呼び出し回数が、本番でも同じだと想定しないでください。