検索フォームで学ぶReact:入力のstate・確定した条件・子へのprops
検索ボタンで確定する仕様を、Reactのstateとpropsでどう実装するか。入力ごとに絞る方式への変更方針と、実際の教材検索の設計・検証記録を読めます。
制作・運営:React Guide 運営者
Reactの実装を読む
検索の仕様を、stateとpropsに落とし込む
ここでは入力途中のdraftと、検索時に確定するsubmittedを別々のstateに持ちます。結果はsubmittedから計算して、子へpropsで渡します。ボタンで確定する仕様を、どの値と処理で実現するか確認します。
- 入力中のstate:draft
- 確定済みのstate:submitted
子コンポーネントが受け取る結果
- stateの更新
- propsで値を渡す
- Effectと通信
入力中と確定済みの値を上の表示で確認できます。入力のたびに絞る仕様へ変えるなら、resultsの計算元を何に変え、どのstateが不要になるでしょう?
本番検索からURL連携と全教材データを除いた学習用の実装です。入力・予想・説明はこの画面内だけで扱い、保存・送信・自動採点しません。再読み込みすると消えます。
この教材で理解すること
前提は、JavaScriptの関数・配列と、ReactのuseStateの基本です。冒頭の実験は、入力・確定・結果の3つを観察するための小さなReactアプリです。
- 入力中の値と、検索を確定した値には、別々の役割がある。
- stateの更新で再レンダーされても、検索結果の内容まで変わるとは限らない。
- 確定済みの条件から計算できる結果は、別のstateに保存しなくてもよい。
- 子はpropsで結果を受け取り、親が検索条件を持つ。
検索ボタンを押して結果を変えるのは、利用者には普通の動作です。この教材では、その動作を実装する値の持ち方を読み解きます。入力のたびに絞る仕様へ変更するなら、resultsをdraftから計算し、submittedと確定処理を外せます。2つのstateは常に必要なのではなく、今回選んだ仕様に対応しています。
入力から表示までを追う
実験の親コンポーネントは、入力中の draft と、確定済みの submitted を持ちます。
const [draft, setDraft] = useState('')
const [submitted, setSubmitted] = useState('')
const results = lessons.filter(lesson =>
lesson.toLowerCase().includes(submitted.toLowerCase())
)
// 入力欄:value={draft}、onChangeでsetDraftを呼ぶ
// フォーム送信:setSubmitted(draft.trim())を呼ぶ
// 結果の表示:<Results items={results} />これは実験の抜粋です。実験コンポーネントのソース全文には入力欄と子コンポーネントも含まれます。
入力したときはdraftが変わり、親が再レンダーされます。resultsの計算も実行されますが、使っているsubmittedは前のままなので、一致する教材は同じです。filterは新しい配列を作るため「同じ配列オブジェクトだから更新されない」という説明でもありません。
確定ボタンを押すとsubmittedが変わり、次のレンダーでは新しい語で絞ります。Resultsはitemsをpropsとして受け取り、一覧を描画します。Reactが自動的に検索ボタンの意味を理解しているのではなく、どのイベントでどのstateを更新するかを実装で決めています。
結果をもう一つのstateにしたら?
この小さな実験では結果は「教材一覧とsubmitted」から決まります。resultsまでstateで保持すると、検索条件を変えたのに結果側の更新を忘れるなど、同期する箇所が増えます。毎回計算する方が、結果の根拠を一か所にできます。
ただし、サーバーから受け取った結果は、手元の検索語だけから作れるデータではありません。外部の取得結果をstateに持つ場面まで否定しているわけではありません。今回の実験は、固定の3教材をブラウザ内で検索します。
別の画面に使ってみる
解説を閉じて、プロフィール編集を考えてください。入力中の名前は変えたいけれど、保存するまで公開表示は変えたくありません。
- 編集中と公開中の名前を、どこで分けますか?
- 「キャンセル」を押したら、どの値をどの値へ戻しますか?
- 子コンポーネントの公開プロフィールには、どちらの名前をpropsで渡しますか?
コードを思い出せなくても、値の役割と更新するタイミングを説明してみてください。翌日にもう一度、検索の例を見ずに考えれば、解説を読んだ直後とは違う確認になります。回答は自動採点せず、学習効果が確認済みという扱いにもしていません。
実験と本番検索の違い
実験は状態の役割を見せるため、入力中の値もReactのstateで持ちます。本サイトの本番検索は、入力中の値をDOMの入力欄に持ち、確定した値をURLに残します。これは同じコードではありません。
共通するのは「編集中と確定済みを分ける」という設計です。学習用のuseState版を理解した後に、再読込や戻る操作の要件が加わると保存場所がどう変わるかを、以下の制作記録で確認できます。
作ったものと、この記録の立場
このサイトの記事一覧に、教材検索を追加しました。完成した検索を開くと、シリーズ内の教材も含めて探せます。
これは、この機能をAIコーディングエージェントで実装し、自動テストした記録です。運営者が長年の実務で身につけたノウハウを語る記事でも、人による監修済みという記録でもありません。下の仕様と設計判断は、この制作でエージェントが具体化したものです。運営者による個別の承認・実機評価は未実施です。
最初の依頼は「制作と検証の過程を教材にする」という方針への了承でした。詳細な検索仕様まで指示されていたわけではありません。ここで重要なのは、AIに任せた判断を読者から見えなくしないことです。
実装前に、完成条件を言葉にする
「検索欄をつけて」だけでは、次の答えが決まりません。今回は小さな機能として、こう定義しました。
| 判断すること | 今回の仕様 | 選んだ理由 |
|---|---|---|
| 対象 | 単発記事とシリーズ内の全教材 | 記事一覧に出ていないHooks教材も探したい |
| 検索する場所 | タイトル・説明・シリーズ名・タグ | 本文まで含めると、例として触れただけの語でも一致する |
| 複数の語 | 空白で分け、すべて含むものを返す | 「useEffect 通信」のように絞り込める |
| 英字の表記 | 大小文字と全角・半角を揃える | USEEFFECTでもuseEffectを探せる |
| 空欄 | 全教材を表示 | 条件を減らして一覧へ戻れる |
| 0件 | 0件と明示し、語を減らす案内を出す | 故障や読み込み中と区別する |
| 確定のタイミング | 検索ボタン、または通常のフォーム送信 | 入力するたびに結果やURLを変えない |
| 戻る・再読込 | URLのqを確定済みの検索語にする | 同じ条件を再現できる |
| タグ付きの入口 | タグ内で検索し、その状態を明示 | 既存の絞り込みを勝手に消さない |
これらはReactが自動的に決めてくれることではありません。例えば「空欄では何も表示しない」という別の仕様も成立します。AIが動くものを作っても、欲しかった仕様かどうかは別に確認する必要があります。
既存コードを読むと、対象の落とし穴が見えた
改修前の記事一覧は、通常は単発記事だけを返す getAllArticles() を使っていました。一方、タグの絞り込みではシリーズ記事も返していました。
そのまま通常一覧の配列を絞るだけでは、シリーズ内にある useEffect の基本記事を検索できません。これは今回の変更前にコードを読んで見つけた構造上の違いです。「AIが一度失敗する実装を出した」という出来事を後から作ったものではありません。
そこで getSearchableArticles() を追加し、単発記事とシリーズ記事を集めます。シリーズ記事は seriesSlug を残し、検索結果から正しいURLへ移動できるようにします。
読者が同じ状況に出会ったら、検索ロジックより先に「そもそも候補に入っているか」を調べてください。候補にない記事は、検索条件を直しても出てきません。
検索そのものは、Reactの外に置く
検索対象と文字列から結果を計算する処理は、普通のTypeScript関数にしました。
function normalize(value: string): string {
return value.normalize('NFKC').toLowerCase().trim()
}
// articlesはタイトル・説明・シリーズ名・タグを持つ教材の配列
const terms = normalize(query).split(/\s+/).filter(Boolean)
const results = articles.filter(article => {
const text = normalize([
article.title, article.description,
article.series ?? '', ...article.tags,
].join(' '))
return terms.every(term => text.includes(term))
})これは実装の中心部分の抜粋です。型を含む実装全文も確認できます。
every() を使うため、すべての語が見つかった記事だけ残ります。空欄のときは語が0個になり、すべての候補が条件を満たします。正規表現として検索語を実行しないため、.* と入力しても全件一致にはなりません。
NFKCは全角英字以外の互換文字も変換します。原文を保存する処理には使わず、比較用の文字列だけに使っています。ひらがな・カタカナや同義語をすべて同一視する機能ではありません。MDNのnormalizeの説明にある変換の範囲も確認できます。
検索結果を別のstateに保存して、Effectで毎回同期する構成にはしていません。候補と確定した検索語からその場で計算できます。React公式の説明でも、表示用のデータ変換に不要なEffectを加えない考え方が紹介されています。
入力中の語と、検索済みの語を分ける
入力欄は編集中の値を持ち、検索の確定時に FormData で読み出します。確定した値は URLSearchParams の q に保存します。
const next = new URLSearchParams(params)
next.set('q', String(data.get('q') ?? '').trim())
setParams(next)既存のparamsをコピーするので、タグ条件は残ります。URL文字列へ検索語を直接連結しないため、& などを検索語として渡す際もURLのエンコードを任せられます。
URLを変更するとブラウザの履歴にも条件が残ります。ただし、入力欄に defaultValue を設定するだけでは、履歴で戻ったときに値が追従するとは限りません。今回はフォームの key を確定済みの検索語とタグから作り、条件が変わったときに入力欄を作り直します。
その結果、未送信の編集内容は、別の検索条件へ戻った際に破棄されます。今回は「確定した検索を再現する」を優先しました。下書きを残したいフォームでは別の設計が必要です。
独自のEnterキー処理は追加していません。ただし、それだけで全OSの日本語IMEで正しく動くと保証はできません。今回の自動テストは日本語文字列の入力を確認したもので、実際の変換候補の選択や変換確定を再現したものではありません。
読者が再現できる確認手順
- 教材検索で
USEEFFECTを入力して検索する。シリーズの基本記事が見つかるか確認する。 存在しない教材xyzで検索する。0件の説明が出るか確認する。- ブラウザで戻る。前の検索語が入力欄へ戻り、結果も戻るか確認する。
- 再読み込みする。同じ検索語と結果になるか確認する。
- useEffectタグから
通信で検索する。URLにタグと検索語の両方が残るか確認する。 - タグを解除して空欄で検索する。単発記事だけでなくシリーズの教材も表示されるか確認する。
- スマートフォン幅で、検索欄とボタンが画面の外へはみ出さないか確認する。
「検索ボタンが押せた」と「欲しい教材を探せた」は同じではありません。検索結果に出るはずの具体的な記事を一つ決めて確認します。
実施した検証と、残っている確認
2026年9月12日、Node.js 24.6.0、React 19.2.5、React Router 7.14.2、Playwright 1.60.0の環境で実行しました。ブラウザ検証はPlaywrightのChromiumです。
| 検証 | 実施結果 |
|---|---|
| 検索関数の5テスト | 空欄、全角・大小文字、複数語、説明・シリーズ名、正規表現風の入力がすべて成功 |
| 検索のブラウザテスト | シリーズ記事の発見、0件、履歴で戻る、再読込、タグ併用、空欄、390px幅の確認が成功 |
| サイト全体の単体テスト | 13ファイル・155テスト成功 |
| ビルド後のページ検査 | 73ページのメタ情報・内部リンク・アンカーの検査が成功 |
| 既存教材を含むブラウザテスト | 4テスト成功 |
今回の自動テストでは失敗は検出されませんでした。意図的な不具合を混ぜて「AIの失敗→修正」という筋書きにはしていません。仕様を先に決め、実装後に確かめた例です。
実装と照合したい方は、検索画面のソースと検索関数のテストを参照してください。これらはビルド時に実ファイルから出力しています。画面のソースは本サイトのルーター・教材データ・コンポーネントに依存し、単独で動く完成プロジェクトではありません。
本リポジトリで実行したコマンドは次のとおりです。ブラウザテストにはPlaywrightのブラウザが必要です。
npm run lint
npm run test
npm run build
npm run check:quality
npm run test:reviewこの機能はブラウザ内の教材データを検索します。検索用の外部APIはありません。ただし検索語はURLに入るため、再読み込み時のサーバーログや既存のアクセス解析などにURLが記録される可能性があります。秘密情報を検索語として入力する用途ではありません。
未確認なのは、実機の日本語IME、スクリーンリーダーでの使い勝手、大量の記事での速度、読者が実際に使う検索語の十分な網羅です。自動テストが通ることと、検索体験が良いことは分けて考えます。
Reactに絞って学ぶ意味と、その限界
今回の文字列処理はReact専用ではありません。Reactを使っているのは、入力欄・URL・結果の表示を既存サイトへ組み込む部分です。
このサイトのReact環境で一つの機能を作ることで、要求から表示、検証までを同じ題材で追えます。検索対象を決める、期待する記事を決める、未確認を残すという手順は他のフレームワークでも使えます。ReactだからAIの出力が正しくなる、という結論にはなりません。
次に取り組むなら、実際に探したかった教材と検索語を数件集め、見つからなかった理由を分類します。表記の違いなのか、説明に語がないのか、そもそも教材がないのか。機能を追加する前に、その違いを見つけることが次の判断材料になります。
同じテーマの教材
TanStack Start v1を試す:型安全フルスタックReact
v1リリース候補になったTanStack Startを実際に試す。プロジェクト作成からファイルベースルーティング、Server Functionsまでを動かしながら解説する。
React 19.2の<Activity>とuseEffectEventを試す
React 19.2で安定版になったActivityとuseEffectEventを実際に試す。タブを切り替えても状態が消えないUIと、useEffectの依存配列の悩み解消を動かしながら解説する。
React Compiler 1.0:Viteへの導入とメモ化の確認方法
安定版になったReact Compilerを実際に試す。Vite 8環境への導入からDevToolsでの効果確認、既存のuseMemoとの付き合い方までを動かしながら解説する。