
はじめに
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では現在積極採用をしています。 ぜひお気軽にご応募ください。
UPSIDER Engineering Deckはこちら📣