TT Lab
はじめる
学ぶ 学習パス コース

クジラを探したのに、猫が現れた

クジラ1万頭を画面に連れてくる方法

TT Labで続きを見る

目標

React+TypeScriptで、検索フォームと状態の案内、50件ずつ見られる一覧を、自分で作成します。 画面が更新されても、入力・フォーカスが維持されるかを検査し、ブラウザで直接操作します。

なぜ重要なのか

正確なレスポンスを受け取っても、入力に名前がなかったり、キーボードで送信できなかったりすれば、使えない画面です。 エラーと空の結果の案内は、ユーザーが次の行動を選ぶのを助けます。大量のデータは、ネットワークの受信コストとDOMのレンダリングコストを、別々に生みます。 すべてのデータをCSSで隠すことと、実際に一部だけを描くことは別です。JSX・props・useState・条件付きレンダリングと、TypeScriptのユニオンが読める必要があります。

準備

最初に1回、node /opt/react-workbench/prepare.mjs /root/react-ui-lab uiを実行してください。 既存のディレクトリがある場合は、現在の作業をそのまま使い続け、上書きしないでください。npm installは不要です。 編集するファイルは/root/react-ui-lab/src/SearchView.tsx、型の素材は/root/react-ui-lab/src/types.tsです。 SearchViewは、state(SearchState)、revision(number)、onSearch(text)をpropsとして受け取るnamed exportです。 SearchStateのsuccessのitemsは1個以上で、0件はemptyです。revisionは、結果が新しく注入されるたびに変わります。 右側の状態注入装置は、HTTPなしで合成データを提供します。hookの正解を書き直さなくても、UIを練習できます。

ステップ

  1. 検索ボックスに名前を与える: uiモードで/root/react-ui-labを準備し、src/SearchView.tsxを作成してください。SearchViewのnamed exportと、提供されたpropsを維持します。roleがsearchのform、「検索語」というlabelと関連付けられた最大80文字のinput、「検索」というsubmit buttonを作ってください。入力値はコンポーネントで管理します。このステップでは、送信の処理をまだつなぎません。
  2. Enterでも同じ検索を送る: formの送信によるデフォルトのリロードを防ぎ、前後の空白を取り除いた入力値をonSearch(text)に渡してください。同じ単語を連続して提出しても毎回呼び出し、空白だけを入力した場合は空文字列を渡します。入力中の文はそのまま維持し、ボタンのクリックだけに頼らないでください。
  3. 空の書庫と壊れた書庫を区別する: 常に存在する状態案内の領域に、role=status、aria-live=polite、aria-atomic=trueを置いてください。idle/loading/empty/error/successを、それぞれ異なる韓国語の文で説明します。待機・空の結果・成功には該当するquery、失敗には渡されたmessageを表示してください。正常な状態でなければ、前の結果の一覧を残しません。
  4. データをコードではなく文字として見せる: successのitemsごとに、titleとdetailを元の順序で表示してください。「検索結果の一覧」という名前のulを作り、tabIndex=0で一覧もフォーカスを受けられるようにしてください。タイトルにHTMLのような文字列が入ってきても、タグとして実行せず、そのまま文字として表示します。このステップでは、全体の一覧を表示してもかまいません。
  5. クジラ1万頭を50頭ずつ見る: 元のitemsを保存したまま、一度に最大50個の項目だけをDOMに描いてください。「前のページ」「次のページ」という基本のbuttonで、表示範囲を変更します。1万件で、次のページは51番目のデータから始まり、再び前へを押すと最初のデータに戻る必要があります。数字やCSSの表示だけを変えて、DOMにすべて残す方式は除外します。
  6. 最後のページの先に落ちない: 「結果ページ」という名前のnavの中に、ページ操作をまとめてください。最初のページの「前」と、最後の「次」は、disabledである必要があります。51件なら最後に1件、ちょうど50件なら1ページです。success.itemsは空ではなく、0件の結果はemptyとして渡されるという契約を使います。
  7. 新しい検索に古いページ番号を持ち越さない: revisionが変わったら、結果は最初のページから見せてください。同じqueryの再検索も、新しいrevisionです。3ページ目から3件の新しい結果に変わっても、空の画面ではなく、最初のデータが出る必要があります。入力中の文と検索フォームの寿命は、結果の一覧とは分けます。
  8. 画面が更新されても指の位置を守る: 入力中に新しい結果や失敗が届いても、入力DOM、書きかけの文、フォーカスが維持されるかを検査してください。状態案内のDOMも維持し、文言だけを更新します。前のステップの契約をすべて再検査します。ビルド後のプレビューで、Tab・Enter、ページ移動、390pxの画面を自分で確認してください。これは回帰のステップなので、ステップ7の正しい実装で通過できます。

参考

cd /root/react-ui-labのあと、npm run typecheck、npm run build、npm startの順に実行します。 プレビューのアドレスhttp://127.0.0.1:3000/の末尾の/は残してください。ソースを変更したら、再ビルドして再読み込みします。 ステップ1では、まだform送信の処理がないため、Enterでリロードされることがあります。ステップ2でつなぎます。 node /opt/react-workbench/grading/check-step.mjs /root/react-ui-lab/src/SearchView.tsx 8 uiで、全体の累積の契約を検査できます。 各ステップは、前のステップも一緒に検査します。一般のソースは128KiB、作業プロセスは12秒の制限で、採点は学習者のファイルを変更しません。 このラボのDOM検査は、CSSの可視性・色のコントラスト・実際のスクリーンリーダーの読み上げの認証ではありません。 50件ずつ表示しても、受信した配列はメモリに残ります。HTTPの時間・React ProfilerのactualDuration・レイアウト・ペイントは、区別して観察してください。 必要なら+時間で延長し、終了前にソースを別に保存してください。ラボのセッションが終わると、ファイルは保持されません。

検索ボックスに名前を与える

uiモードで/root/react-ui-labを準備し、src/SearchView.tsxを作成してください。SearchViewのnamed exportと、提供されたpropsを維持します。roleがsearchのform、「検索語」というlabelと関連付けられた最大80文字のinput、「検索」というsubmit buttonを作ってください。入力値はコンポーネントで管理します。このステップでは、送信の処理をまだつなぎません。

labelを画面に書くことと、入力欄に関連付けることは別です。htmlFor/idのペアか、labelの中にinputを入れる方式を使ってください。placeholderだけで代用しません。

Enterでも同じ検索を送る

formの送信によるデフォルトのリロードを防ぎ、前後の空白を取り除いた入力値をonSearch(text)に渡してください。同じ単語を連続して提出しても毎回呼び出し、空白だけを入力した場合は空文字列を渡します。入力中の文はそのまま維持し、ボタンのクリックだけに頼らないでください。

検索の意図は、入力イベントではなくform送信で作ります。onSubmitで処理すれば、デフォルトのsubmitボタンとEnterが同じ経路を使います。

空の書庫と壊れた書庫を区別する

常に存在する状態案内の領域に、role=status、aria-live=polite、aria-atomic=trueを置いてください。idle/loading/empty/error/successを、それぞれ異なる韓国語の文で説明します。待機・空の結果・成功には該当するquery、失敗には渡されたmessageを表示してください。正常な状態でなければ、前の結果の一覧を残しません。

emptyは、検索に成功したがデータがないという意味で、errorはリクエストが失敗したという意味です。同じ案内にまとめると、ユーザーは検索語を変えるのか再試行するのかを決められません。

データをコードではなく文字として見せる

successのitemsごとに、titleとdetailを元の順序で表示してください。「検索結果の一覧」という名前のulを作り、tabIndex=0で一覧もフォーカスを受けられるようにしてください。タイトルにHTMLのような文字列が入ってきても、タグとして実行せず、そのまま文字として表示します。このステップでは、全体の一覧を表示してもかまいません。

JSXのテキストの位置に値を入れれば、データがHTMLとして解釈されません。一覧のkeyにはデータの安定したidを使い、説明も忘れないでください。

クジラ1万頭を50頭ずつ見る

元のitemsを保存したまま、一度に最大50個の項目だけをDOMに描いてください。「前のページ」「次のページ」という基本のbuttonで、表示範囲を変更します。1万件で、次のページは51番目のデータから始まり、再び前へを押すと最初のデータに戻る必要があります。数字やCSSの表示だけを変えて、DOMにすべて残す方式は除外します。

現在のページ番号から、sliceの開始と終了を計算してください。データの受信量はそのままで、レンダリングする行の数だけを減らすという点を区別します。

最後のページの先に落ちない

「結果ページ」という名前のnavの中に、ページ操作をまとめてください。最初のページの「前」と、最後の「次」は、disabledである必要があります。51件なら最後に1件、ちょうど50件なら1ページです。success.itemsは空ではなく、0件の結果はemptyとして渡されるという契約を使います。

割り算を切り上げると、必要なページ数が得られます。最後のインデックスとページ数は、1だけ違います。色を薄くするだけでなく、実際のdisabled属性を使ってください。

新しい検索に古いページ番号を持ち越さない

revisionが変わったら、結果は最初のページから見せてください。同じqueryの再検索も、新しいrevisionです。3ページ目から3件の新しい結果に変わっても、空の画面ではなく、最初のデータが出る必要があります。入力中の文と検索フォームの寿命は、結果の一覧とは分けます。

結果用の子コンポーネントだけを新しいrevisionで再生成すれば、ページの状態を初期化できます。フォームまでkeyで作り直すと、入力とフォーカスも一緒に消えます。

画面が更新されても指の位置を守る

入力中に新しい結果や失敗が届いても、入力DOM、書きかけの文、フォーカスが維持されるかを検査してください。状態案内のDOMも維持し、文言だけを更新します。前のステップの契約をすべて再検査します。ビルド後のプレビューで、Tab・Enter、ページ移動、390pxの画面を自分で確認してください。これは回帰のステップなので、ステップ7の正しい実装で通過できます。

フォーカスを結果へ強制的に移しません。DOMの自動検査と、実際のブラウザでのキーボード・レイアウトの観察は、別々の証拠であり、スクリーンリーダーの使いやすさまで認証するものではありません。