useEffectは、コンポーネントが画面に描画された後に、Reactの外にあるもの――APIからのデータ取得、タイマー、ブラウザのイベントの購読、外部ライブラリの操作など――と同期させるためのフックです。第2引数の依存配列で、「いつ実行するか」を指定します。

一方で、useEffectは使わなくてよい場面で使われやすいフックでもあります。Reactの公式ドキュメントも、「エフェクトは必要ないかもしれない」という章を設けて、不要なuseEffectが不具合や無駄な再描画の原因になると説明しています。この記事では、基本の使い方と落とし穴に加えて、useEffectを使わずに書くべき場面と、React 19以降の選択肢までを解説します。

基本の書き方

import { useEffect, useState } from "react";

function Clock() {
  const [now, setNow] = useState(new Date());

  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 1000);   // 実行したい処理
    return () => clearInterval(id);                           // 後片付け(クリーンアップ)
  }, []);                                                      // 依存配列

  return <p>{now.toLocaleTimeString()}</p>;
}
部分 役割
第1引数の関数 描画の後に実行する処理
戻り値の関数(クリーンアップ) 次に処理を実行する前と、コンポーネントが消えるときに実行する後片付け
第2引数(依存配列) どの値が変わったときに、処理を実行し直すか

依存配列の3つの書き方

書き方 実行されるタイミング
useEffect(() => {...})(配列なし) 描画のたびに毎回
useEffect(() => {...}, [])(空の配列) 最初に表示されたとき1回だけ(と、消えるときのクリーンアップ)
useEffect(() => {...}, [a, b]) 最初と、a または b が変わったとき
useEffect(() => {
  document.title = `${count}件の通知`;
}, [count]);   // count が変わるたびにタイトルを更新する

依存配列には、エフェクトの中で使っている、props・state・コンポーネント内で定義した値をすべて入れるのが原則です。入れ忘れると、古い値のまま処理が動く不具合になります。ESLintの react-hooks/exhaustive-deps ルールを有効にしておくと、入れ忘れを警告してくれます。

クリーンアップ(後片付け)

タイマー、イベントの購読、接続などを始めたら、必ずクリーンアップで止めます。

useEffect(() => {
  function handleResize() {
    setWidth(window.innerWidth);
  }
  window.addEventListener("resize", handleResize);
  return () => window.removeEventListener("resize", handleResize);
}, []);

クリーンアップを書き忘れると、画面を移動した後もタイマーやイベントが動き続け、メモリを使い続けたり、消えたコンポーネントの状態を更新しようとしたりします。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「ReactのuseEffectの使い方|依存配列と使わなくてよい場面」

開発中にuseEffectが2回実行される理由

React 18以降、開発環境で <StrictMode> を使っていると、コンポーネントを「表示→いったん片付け→もう一度表示」します。そのため、空の依存配列のuseEffectも、開発中は2回実行されたように見えます。

これは、クリーンアップの書き忘れを見つけるための、意図的な動きです。本番のビルドでは1回だけ実行されます。

2回実行されて困ること 意味すること 対処
イベントが2重に登録される クリーンアップで解除していない クリーンアップを書く
APIが2回呼ばれる データ取得の処理は、2回呼ばれても問題ない作りにする 後述の競合対策、またはデータ取得ライブラリを使う
分析用のイベントが2回送られる 開発時の動きなので、本番には影響しない 気にしなくてよい(開発時は送らない設定にする)

StrictModeを外して2回実行を止めるのは、おすすめできません。クリーンアップの不足という本当の問題が隠れてしまいます。

無限ループになる原因

原因1:依存配列なしで、stateを更新している

useEffect(() => {
  setCount(count + 1);   // 描画 → 更新 → 描画 → 更新 …が止まらない
});

原因2:依存配列に、毎回作り直されるオブジェクトや関数を入れている

function Search({ keyword }) {
  const options = { keyword, limit: 10 };   // 描画のたびに新しいオブジェクトになる

  useEffect(() => {
    fetchResults(options).then(setResults);
  }, [options]);   // 毎回「変わった」と判定され、取得 → 描画 → 取得…が続く
}

Reactは、依存配列の値を Object.is で比べます。中身が同じでも、新しく作られたオブジェクトや関数は「違う値」と判定されます。

対処

  • オブジェクトをエフェクトの中で作り、依存配列には元になる値(keyword)だけを入れる
  • 関数は、エフェクトの中で定義する
  • どうしても外で作る必要がある場合は、useMemo・useCallback で同じ値を保つ
useEffect(() => {
  const options = { keyword, limit: 10 };
  fetchResults(options).then(setResults);
}, [keyword]);

データ取得と、応答の順番の競合

useEffectでAPIからデータを取得する場合、古いリクエストの応答が、新しいリクエストの応答より後に届くことがあります。検索キーワードを素早く変えたときに、古いキーワードの結果が表示される不具合です。

useEffect(() => {
  let ignore = false;                       // この実行が古くなったかどうか

  async function load() {
    const res = await fetch(`/api/search?q=${encodeURIComponent(keyword)}`);
    const data = await res.json();
    if (!ignore) setResults(data);          // 古い実行の結果は無視する
  }
  load();

  return () => { ignore = true; };          // 次の実行の前に「古い」と印を付ける
}, [keyword]);

useEffectに渡す関数そのものは async にできないため(クリーンアップの関数を返す必要があるため)、中でasync関数を定義して呼び出します。通信自体を止めたい場合は、AbortController を使って、クリーンアップで abort() します。

実務では、キャッシュ、再試行、読み込み中の状態の管理まで必要になるため、TanStack Query(React Query)やSWRなどのデータ取得ライブラリや、Next.jsなどフレームワークのデータ取得の仕組みを使うのが一般的です。

useEffectを使わなくてよい場面

Reactの公式ドキュメントでは、「外部のシステムと同期する必要がないなら、エフェクトは不要」とされています。よくある不要なuseEffectと、その書き換え方です。

1. 他のstateやpropsから計算できる値

// × 不要なuseEffect:描画が2回になり、一瞬古い値が表示される
const [fullName, setFullName] = useState("");
useEffect(() => {
  setFullName(`${lastName} ${firstName}`);
}, [lastName, firstName]);

// ○ 描画中に計算する
const fullName = `${lastName} ${firstName}`;

計算が重い場合は、useMemo で結果を再利用します。

2. ユーザーの操作に応じた処理

// × 送信ボタンの処理をuseEffectで行う
useEffect(() => {
  if (submitted) postForm(data);
}, [submitted]);

// ○ イベントハンドラーで直接行う
function handleSubmit() {
  postForm(data);
}

「ボタンを押したから」行う処理は、イベントハンドラーに書きます。「画面に表示されたから」行う処理だけが、エフェクトの対象です。

3. propsが変わったときにstateをリセットする

// × useEffectでリセットする
useEffect(() => {
  setComment("");
}, [userId]);

// ○ key を変えて、コンポーネントごと作り直す
<Profile userId={userId} key={userId} />

4. エフェクトの連鎖

「Aが変わったらBを更新するエフェクト」「Bが変わったらCを更新するエフェクト」のように連鎖させると、描画が何度も発生し、処理の流れも追いにくくなります。1つのイベントハンドラーの中で、まとめて計算・更新します。

やりたいこと useEffectの代わりに
他の値から計算できる値 描画中に計算(重ければ useMemo)
ボタンなどの操作に応じた処理 イベントハンドラー
propsの変更でstateをリセット key を変える
親に変更を通知する イベントハンドラーの中で親の関数を呼ぶ
外部のストアの購読 useSyncExternalStore
データ取得 データ取得ライブラリ、フレームワークの仕組み

useLayoutEffectとの違い

useEffect useLayoutEffect
実行のタイミング 画面に描画された後 画面に描画される前
画面の表示 待たせない 処理が終わるまで表示を待たせる
使う場面 ほとんどの場合 要素の大きさを測って位置を調整するなど、ちらつきを防ぎたい場合

useLayoutEffect は描画を止めるため、必要な場面に限って使います。

React 19以降の選択肢

React 19以降では、useEffectで書いていた処理の一部に、別の書き方が用意されています。

  • use:Promiseの結果を、Suspenseと組み合わせて読み取れる
  • Actions・useActionState:フォームの送信と、その送信中・結果の状態をまとめて扱える
  • useEffectEvent(React 19.2で安定版):エフェクトの中で「最新の値を読みたいが、その値の変化では再実行したくない」処理を分けられる
  • サーバーコンポーネント(Next.jsなど):データ取得をサーバー側で行い、useEffectでの取得そのものを不要にできる

新しく作るプロジェクトでは、データ取得やフォームの処理を、まずこれらの仕組みで書けないかを検討すると、useEffectの落とし穴を避けやすくなります。

よくある質問

Q. useEffectの中でasync/awaitを使うにはどうすればいいですか? A. useEffectに渡す関数そのものはasyncにできないため、中でasync関数を定義して呼び出します。応答の順番の競合を防ぐため、クリーンアップで「古い結果を無視する」処理も入れておきます。

Q. 依存配列の警告を無視してもいいですか? A. 無視すると、古い値のまま処理が動く不具合の原因になります。警告が出る場合は、値をエフェクトの中に移す、エフェクトを分ける、そもそもエフェクトが不要でないかを見直す、のいずれかで解決するのがおすすめです。

Q. 空の依存配列なら、1回しか実行されないと考えてよいですか? A. 本番では、表示されたときに1回ですが、開発中はStrictModeで2回実行されます。また、コンポーネントが一度消えて再び表示されれば、その都度実行されます。「1回しか実行されない」前提の処理(2重登録すると困る処理)は、クリーンアップで対になる処理を書きます。

Q. クラスコンポーネントのcomponentDidMountと同じですか? A. 似ていますが、同じではありません。useEffectは「ライフサイクルのタイミング」ではなく、「どの値と同期するか」で考えるのが、現在のReactの考え方です。

まとめ

  • useEffectは、描画の後にReactの外の世界と同期するためのフック
  • 依存配列は、なし=毎回、[]=最初だけ、[a]=aが変わったとき。使う値はすべて入れる
  • タイマーやイベントの購読は、クリーンアップで必ず止める
  • 開発中の2回実行はStrictModeの意図的な動き。クリーンアップの不足を見つけるためのもの
  • オブジェクトや関数を依存配列に入れると無限ループになりやすい
  • データ取得は応答の順番の競合に注意。実務ではデータ取得ライブラリやフレームワークの仕組みを使う
  • 計算できる値・操作に応じた処理・stateのリセットには、useEffectを使わない

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「ReactのuseEffectの使い方|依存配列と使わなくてよい場面」

株式会社RJCでは、AI駆動開発の場面でも、生成されたコードが正しい設計になっているかを見極める力を大切にしています。こうした知識を仲間と共有しながら、一緒に成長を楽しんでいます。

株式会社RJC ― SI事業・SES事業・AI駆動開発。未経験からエンジニアに育てるのが得意な会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?