React / Next.js公開 約10分

React useEffectを使う前に:計算・クリック・外部同期を分ける

絞り込み結果をuseEffectでstateへ写していませんか。小さな資料検索で、表示用の計算、クリックによる確定、document.titleとの同期を分けます。条件変更と部品の終了で、必要な処理だけが動くか確認します。

この記事の目次
  1. 三つの処理を、実行する理由で仕分ける
  2. 計算した件数を、そのまま一覧とタブ名へ渡す
  3. 件数が同じでも、計算と外部同期は別々に動ける
  4. 古い一覧と、勝手に変わる確定記録を直す
  5. 計算が重いときも、まず計測する対象を決める
  6. 検索を閉じた後のタブ名も確かめる

検索語を変えたのに件数だけ古い、一覧を絞る処理へ条件を足すたびに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

記事の内容を、課題で確かめる

前提になるJavaScriptの練習 →

React・TypeScriptの学習内容 →

記事についてのお問い合わせ

記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。

← 記事一覧に戻る