この記事の目次
検索語を変えたのに件数だけ古い、一覧を絞る処理へ条件を足すたびにEffectの依存配列も直す。そんな画面では、表示用に計算できる値までstateに保存しているかもしれません。処理が何をきっかけに必要になるかを分けてみます。
JSX、useState、配列のfilterが分かる人向けです。架空の資料検索を作り、計算・利用者の操作・ブラウザーとの同期を同じ画面で比較します。通信は行いません。React / React DOM 19.2.0、Vite 7.1.7、Node.js 24系へ固定した練習です。
三つの処理を、実行する理由で仕分ける
検索語と資料が決まれば結果は一意に計算できます。結果だけを後から変更する操作はありません。それなら、現在の値からレンダー中に計算すれば、別のstateを追い掛けて更新する必要がなくなります。React公式のEffectを減らす説明でも、表示用の変換とイベント処理を分けています。
| 処理 | きっかけ | 置く場所 |
|---|---|---|
| 一致する資料と件数を求める | 今回の検索語で表示する | コンポーネント内の計算 |
| この結果を確認済みとして記録する | 確定ボタンを押した | onClick |
| ブラウザーのタブ名を件数へ合わせる | 表示する件数が変わった | useEffect |
確定済みの件数は、検索語を変えても前の値を残したい情報です。現在の件数から常に作り直す値ではないのでstateで保持します。同じ数値でも「今の結果」か「押した時点の記録」かで置き場所が変わります。
共通の親へstateを移す記事が扱うのは値の持ち主です。今回は持ち主を移す前に、その値をそもそも保持する必要があるか、外部へ反映する必要があるかを判断します。
計算した件数を、そのまま一覧とタブ名へ渡す
空の練習フォルダで次を実行します。既存プロジェクトの依存関係は変更しません。
npm init -y
npm install --save-exact react@19.2.0 react-dom@19.2.0
npm install --save-dev --save-exact vite@7.1.7
index.htmlを保存します。
<!doctype html>
<html lang="ja"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>資料検索の練習</title>
<style>
body { margin: 16px; font-family: sans-serif; line-height: 1.8; }
main { max-width: 560px; margin: auto; }
input { box-sizing: border-box; width: 100%; font: inherit; }
button { font: inherit; margin: 8px 0; }
:focus-visible { outline: 3px solid #174ea6; outline-offset: 3px; }
</style></head><body><div id="root"></div>
<script type="module" src="/main.jsx"></script></body></html>
同じ場所のmain.jsxへ次を書きます。TabTitleは画面の要素を返さず、タブ名との同期だけを担当します。
import React, { useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
const materials = ['星の地図', '星の写真', '月の記録'];
function TabTitle({ count }) {
useEffect(() => {
const previous = document.title;
document.title = `${count}件|資料検索`;
return () => { document.title = previous; };
}, [count]);
return null;
}
function Search() {
const [query, setQuery] = useState('');
const [confirmed, setConfirmed] = useState(null);
const keyword = query.trim();
const visible = materials.filter(name => name.includes(keyword));
return <section>
<TabTitle count={visible.length} />
<label htmlFor="query">資料名で検索</label>
<input id="query" value={query} onChange={event => setQuery(event.target.value)} />
<p>現在:{visible.length}件</p>
{visible.length === 0 ? <p>一致する資料はありません。</p>
: <ul>{visible.map(name => <li key={name}>{name}</li>)}</ul>}
<button type="button" onClick={() => setConfirmed(visible.length)}>この結果を確定</button>
<p>{confirmed === null ? '未確定' : `確定時:${confirmed}件`}</p>
</section>;
}
function App() {
const [open, setOpen] = useState(true);
return <main>
<h1>資料検索(架空の練習)</h1>
<button type="button" onClick={() => setOpen(value => !value)}>
{open ? '検索を閉じる' : '検索を開く'}
</button>
{open && <Search />}
</main>;
}
createRoot(document.getElementById('root')).render(<App />);
npx vite --host 127.0.0.1で起動し、表示されたURLを開きます。初期状態は三件、検索語「星」は二件です。入力欄は元の文字を保持し、検索するときだけtrimします。大文字小文字や全角の統一は行わない、部分一致の見本です。
件数が同じでも、計算と外部同期は別々に動ける
visibleは表示のたびに現在のqueryから計算します。「星」から「月」へ変えると、一覧と現在の件数が同じ材料から作られます。Effectを待ってvisibleを書き直す段階はありません。
タブ名はReactの返すJSXの外にあるdocument.titleなので、ここではEffectで同期します。useEffectの公式資料に従い、使用するcountを依存配列へ入れます。別の検索語でも件数が同じなら、タブ名の同期はやり直す必要がありません。
クリーンアップは、次のcountによる設定の前とTabTitleを外したときに呼ばれます。この見本はタブ名を変更する担当が一つだけという前提で、設定前の文字へ戻しています。別の部品もタブ名を変更するアプリなら、この方式で互いに上書きせず、タイトル管理の担当をそろえます。
古い一覧と、勝手に変わる確定記録を直す
一つ目の失敗は、visibleをuseStateで持ち、空の依存配列のEffectで絞り込むことです。初回だけ計算するため、後から検索語を変えても一覧が追い付きません。queryを依存へ足せば更新はできますが、計算結果を別stateへ写す構造は残ります。完成例の直接計算へ戻し、「星」「月」「海」で二件・一件・零件になるか確認します。
二つ目は、確定記録をuseEffect(() => setConfirmed(visible.length), [visible.length])で更新することです。ボタンを押していないのに、検索するだけで確定済みになります。原因は「表示が変わった」と「利用者が確定した」の混同です。更新をonClickへ戻し、星の二件を確定した後、月を検索しても確定時は二件のままか確かめます。
このとき確定時を一件へ自動更新するのは修正ではありません。仕様上、過去の操作を残す欄だからです。値が違っていることが不具合かどうかは、変数名だけでなく、いつの情報を表すかで決めます。
計算が重いときも、まず計測する対象を決める
三件のfilterにuseMemoは必要ありません。大量の資料で計算が遅いと分かった場合には、同じ入力の計算を再利用する選択があります。ただしuseMemoは計算の最適化であり、確定記録の保存や外部との同期の代わりではありません。
外部イベントの購読やタイマーを加えるなら、Effectの設定と解除を対にします。一方、今回のvisibleには購読も通信もありません。すべての処理をEffectから消すのではなく、外部との関係がある処理に用途を絞ります。開発時のStrict Modeで設定・解除が追加実行されても、後始末が対応する構造にします。
検索を閉じた後のタブ名も確かめる
開発サーバーを止め、npx vite build、npx vite preview --host 127.0.0.1で公開用の結果を開きます。15分程度で、星は二件と対応するタブ名、海は零件と空の案内、空白だけは三件になることを照合します。
さらに星を確定してから月へ変え、現在一件・確定時二件になることを確認します。検索を閉じるとタブ名は「資料検索の練習」へ戻り、開き直すと三件・未確定へ戻るのが期待結果です。検索部品を外すため、その内部のstateも終了します。
Tabで検索欄と各ボタンを操作でき、幅320pxでも結果の文字が読めるかを確認してください。カリキュラムのReact学習へつなぐ際には、三つの処理それぞれについて「何が起きたので実行したか」を説明できれば、この仕分けをほかの画面にも使えます。
TRY A LESSON
記事の内容を、課題で確かめる
記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。