UPSIDER Tech Blog

useEffect は最終手段 — 本当にそれは必要かを考える

はじめに

React を使った開発をしていると、「useEffect を追加したら動いた」 そんなコードに出会ったことはないでしょうか。useEffect はとても身近で、強力な Hook です。外部との同期や副作用の管理を、比較的シンプルに書くことができます。

一方で、使いどころを誤ると、「なぜこの処理がここにあるのか分からない」「少しの変更で挙動が壊れる」といった、扱いづらいコードにもなりがちです。

最近では、AI を使って React のコードを書く機会も増えてきました。useEffect を含む実装も、自然な形で提示されることが多くなっています。だからこそ、その useEffect が本当に必要かどうかは、コードを書く人・レビューする人が判断しなければなりません。

本記事では、useEffect の「書き方」ではなく使うべきかどうかをどう判断するかにフォーカスします。useEffect が出てきたら、「本当にそれは必要か?」と一度立ち止まる。そんな視点を持つきっかけになれば幸いです。

なお、本記事のコード例では、useEffect の挙動と判断に集中するため、JavaScript形式で記載しています。

useEffect の役割を再確認する

useEffect は、React コンポーネントを外部と同期するための Hook です。 React の公式ドキュメントでは、useEffect は escape hatch(非常口) と説明されています。詳しくは参考リンクをご覧ください。render の仕組みだけでは表現できない処理を扱うための手段、という位置づけです。 つまり、React のデータフローの外にある世界とやむを得ずやり取りするための仕組みです。

裏を返すと、次のような処理は useEffect ではなく render の中で完結できる可能性が高いです。

  • state や props から素直に導けるもの
  • render の中で完結できるもの

この前提を押さえておくだけでも、「まず疑う」視点が持てます。

よくある「不要な useEffect」

他の state から計算できる値

useEffect が不要になりやすい代表例が、他の state や props から計算できる値を state として持ってしまっているケースです。

❌ Before:state から計算できる値を useEffect で同期している

function ItemList({ items }) {
  const [query, setQuery] = useState("");
  const [filteredItems, setFilteredItems] = useState([]);

  useEffect(() => {
    setFilteredItems(
      items.filter(item => item.name.includes(query))
    );
  }, [items, query]);

  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <List items={filteredItems} />
    </>
  );
}

この表示用の state は、props と state から毎回導ける値です。つまりこれは、「計算結果」であって、「保存しておく理由のある状態」ではありません。

✅ After:render 内で素直に計算する

function ItemList({ items }) {
  const [query, setQuery] = useState("");

  const filteredItems = items.filter(item =>
    item.name.includes(query)
  );

  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <List items={filteredItems} />
    </>
  );
}

render の中で素直に計算するだけで、state と useEffect を 1 つずつ減らすことができます。

他の state から計算できる値が増えていくと起きること:

  • フィルタ条件が増える
  • 件数表示を追加したくなる
  • 別の状態と組み合わせたくなる

といった変更のたびに、state と useEffect が増えていきがちです。

こうして、state -> useEffect -> state という流れが積み重なり、処理の因果関係が追いにくいコードになっていきます。

ユーザー入力の変化を useEffect で監視しているケース

ユーザーの入力に応じて副作用を実行したいとき、入力値(state)の変化をトリガーに useEffect を書いてしまうことがあります。

例えば、郵便番号が 7 桁になったタイミングで住所を取得するようなケースです。

❌ Before:useEffect でユーザー入力を監視

function AddressForm() {
  const [postalCode, setPostalCode] = useState("");

  useEffect(() => {
    if (postalCode.length === 7) {
      fetchAddress(postalCode);
    }
  }, [postalCode]);

  return (
    <input
      value={postalCode}
      onChange={(e) => setPostalCode(e.target.value)}
      placeholder="郵便番号(7桁)"
    />
  );
}

この実装は動きますが、データフローは間接的になります。

ユーザー操作 -> state 更新 -> useEffect 発火 -> 副作用

入力欄と副作用の処理が離れて見えてしまうことがあります。

✅ After:イベントハンドラで直接実行する

function AddressForm() {
  const [postalCode, setPostalCode] = useState("");

  const handleChangePostalCode = (value) => {
    setPostalCode(value);

    if (value.length === 7) {
      fetchAddress(value);
    }
  };

  return (
    <input
      value={postalCode}
      onChange={(e) => handleChangePostalCode(e.target.value)}
      placeholder="郵便番号(7桁)"
    />
  );
}

イベントハンドラに書いた方が、「いつ・なぜ実行されるのか」が明確になります。

props の変更に合わせて state をリセットする処理

props の変更に合わせて、useEffect で state を初期化しているケースもよく見かけます。 例えば、表示するユーザーが切り替わるたびに編集中のプロフィール文言をリセットしたい、というような場面です。

❌ Before:props の変更を useEffect で吸収する

function Profile({ userId }) {
  const [draftBio, setDraftBio] = useState("");

  useEffect(() => {
    setDraftBio("");
  }, [userId]);

  return (
    <section>
      <h2>User: {userId}</h2>
      <textarea
        value={draftBio}
        onChange={(e) => setDraftBio(e.target.value)}
      />
    </section>
  );
}

一見すると自然な実装に見えますが、いつ state がリセットされるのか、なぜこの useEffect が必要なのか、といった点がコードから直感的に読み取りにくくなりがちです。

✅ After:構造で意図を表現する

function ProfilePage({ userId }) {
  return <Profile key={userId} userId={userId} />;
}

key を使うことで userId が変わったら別の Profile として扱うという意図を React にそのまま伝えることができます。 結果として、state のリセットを useEffect に任せず、「なぜこの処理が動くのか」が構造から分かります。

それでも useEffect が必要なケース

もちろん、useEffect が適している場面もあります。

  • Browser API や外部ライブラリとの同期
  • subscription やイベントリスナーの管理
  • cleanup が必要な副作用

これらは、React の外部と同期するという useEffect 本来の役割に当たります。こうしたケースでは、useEffect を使うこと自体が設計として自然です。

まとめ

useEffect は強力ですが、使いどころを誤るとコードを複雑にします。render で表現できるものは、まず render で書けないかを考えることが重要です。

useEffect が登場したときは、次の点をチェックしてみてください。

  • 他の state や props から計算できる値ではないか
  • ユーザー操作に直接ひもづく処理ではないか
  • props の変更を、構造や key で表現できないか
  • React の外部と同期する必要が本当にあるか
  • render では表現できない理由を説明できるか

useEffect が登場したときに、「本当にそれは必要か?」と一度立ち止まる。 その小さな判断が、コードをシンプルに保ち、将来の変更に強く、レビュワーにも優しい実装につながっていくはずです。

参考リンク

We Are Hiring !!

UPSIDERでは現在積極採用をしています。 ぜひお気軽にご応募ください。

herp.careers

herp.careers

UPSIDER Engineering Deckはこちら📣

speakerdeck.com