useEffectの使い方|React初心者がハマる注意点と不要な書き方

ReactのuseEffectの使い方と依存配列、無限ループ、cleanupの注意点

ReactのuseEffectは、初心者がつまずきやすいHookの1つです。

  • useEffectが無限ループになる
  • APIが何回も呼ばれる
  • 開発環境でuseEffectが2回実行される
  • 依存配列に何を入れればいいかわからない

結論から言うと、useEffectはReactの外側にあるものと同期するためのHookです。API通信、イベント登録、タイマー、localStorage、document.titleの変更など、Reactの描画だけでは完結しない処理で使います。

一方で、計算、表示切り替え、state同士の同期のためだけにuseEffectを書くと、コードが複雑になり、無限ループや余計な再レンダリングの原因になります。

そこでこの記事では、React・Next.jsの実務でよく見る失敗例をもとに、useEffectの使い方、依存配列、Strict Mode、cleanup、AbortController、不要なuseEffectの見分け方を初心者向けに解説します。

結論:useEffectはReactの外側と同期するときに使う

そのため、useEffectを書く前に、まず「これはReactの外側に影響する処理か?」を確認します。

処理useEffectが必要か理由
API通信必要になることが多いサーバーと同期するため
localStorage保存必要になることが多いブラウザAPIと同期するため
イベント登録必要windowやdocumentに登録するため
タイマー開始必要setIntervalやsetTimeoutを管理するため
document.title変更必要になることが多いブラウザのタイトルを変更するため
フルネームの結合不要文字列を計算すればよい
件数の計算不要array.lengthで求められる
合計金額の計算不要reduceで求められる
タブやモーダルの表示切り替え基本的に不要useStateで表現できる

React公式ドキュメントでも、外部システムと同期していないならEffectは不要な可能性が高いと説明されています。つまり、このHookは「とりあえず処理を書く場所」ではありません。

公式情報も確認する

さらに詳しく確認したい場合は、React公式のEffectで同期する考え方不要なEffectを避ける考え方、Next.js公式のApp Routerでのデータ取得ガイドも参考になります。

useEffectを書く前の判断基準

  1. Reactの外側にあるものと同期しているか
  2. 計算だけで済まないか
  3. useStateだけで表現できないか
  4. Next.jsならServer Componentで取得できないか
  5. それでも必要ならuseEffectを書く

useEffectとは?初心者向けに役割を整理

まず、useEffectとは、Reactコンポーネントの描画後に副作用を実行するためのHookです。

import { useEffect, useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `count: ${count}`;
  }, [count]);

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}

例えばこのコードでは、countが変わるたびにブラウザのタブタイトルを変更しています。document.titleはReactの描画そのものではなく、ブラウザ側の状態です。そのためuseEffectで同期しています。

useStateとuseEffectの違い

Hook役割使う場面
useState状態を保存する入力値、表示状態、選択中のタブ
useEffect状態やpropsの変化に応じて副作用を実行するAPI通信、イベント登録、タイマー、ブラウザAPI

useStateは「状態を持つ」ためのHookです。一方で、Effectは「状態変化後にReactの外側へ反映する」ためのHookです。この2つを混同すると、不要な処理が増えやすくなります。

依存配列とは?useEffectの実行タイミングを決める指定

次に、第2引数に書く配列を、依存配列と呼びます。依存配列は、この処理をいつ再実行するかを決める重要な指定です。

書き方実行タイミング
[]初回マウント時に実行初期表示時のデータ取得
[userId]userIdが変わった時に実行ユーザー詳細の再取得
指定なしレンダリングのたびに実行初心者は基本的に避ける

初回だけ実行する例

useEffect(() => {
  fetchUsers();
}, []);

依存配列が空配列の場合、基本的にはコンポーネントが表示された時に実行されます。ただし、開発環境のStrict Modeでは、後述する理由で追加実行されることがあります。

値が変わった時だけ実行する例

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

例えば、userIdが変わったときだけ、対象ユーザーを取得し直します。また、Effectの中で使っているpropsやstateは、基本的に依存配列へ入れると考えると安全です。

依存配列なしは毎回実行される

useEffect(() => {
  console.log("rendered");
});

依存配列を省略すると、レンダリングのたびに実行されます。そのため、意図せずAPIが何度も呼ばれる原因になりやすく、初心者は基本的に避けた方が安全です。

注意点1:useEffectの無限ループを防ぐ

特に、よくある失敗が無限ループです。Effect内で更新しているstateを依存配列に入れると、更新と再実行が繰り返されることがあります。

悪い例:更新するstateを依存配列に入れている

const [count, setCount] = useState(0);

useEffect(() => {
  setCount(count + 1);
}, [count]);

countが変わるとEffectが実行され、その中でsetCountが呼ばれ、またcountが変わります。この流れが繰り返されるため、無限ループになります。

実務でよく見る悪い例:取得結果を依存配列に入れている

const [users, setUsers] = useState([]);

useEffect(() => {
  fetchUsers().then(setUsers);
}, [users]);

fetchUsersの結果でsetUsersを実行するとusersが変わります。その結果、依存配列によってEffectが再実行され、またfetchUsersが呼ばれます。

良い例:取得条件を依存配列に入れる

const [users, setUsers] = useState([]);

useEffect(() => {
  fetchUsers().then(setUsers);
}, []);

初回表示時だけユーザー一覧を取得したいなら、依存配列は空配列にします。一方で、検索条件やページ番号がある場合は、取得結果ではなく検索条件やページ番号を依存配列に入れます。

注意点2:依存配列をごまかさない

依存配列の警告が出たときに、理由がわからないまま空配列にするのは危険です。なぜなら、古い値を参照し続けるstale closureの原因になるからです。

悪い例:userIdを使っているのに依存配列が空

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

このコードでは、userIdが変わってもfetchUserが再実行されません。その結果、画面上では別のユーザーを表示すべきなのに、古いユーザー情報のままになる可能性があります。

良い例:userIdを依存配列に入れる

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

つまり、Effectの中で使っている値が変わったら再実行する。これが依存配列の基本です。なお、ESLintの警告は、依存配列の抜け漏れを見つけるための重要なヒントです。

stale closureとは?

また、stale closureとは、Effect内の関数が古い値を参照し続ける現象です。

useEffect(() => {
  const timerId = setInterval(() => {
    console.log(count);
  }, 1000);

  return () => {
    clearInterval(timerId);
  };
}, []);

countが増えても、初回実行時のcountだけを表示し続ける可能性があります。なぜなら、依存配列が空配列なので、Effectが初回のcountしか知らないためです。

注意点3:Strict ModeでuseEffectが2回実行されることがある

さらに、ReactのStrict Modeが有効な開発環境では、useEffectが2回実行されたように見えることがあります。

環境挙動目的
開発環境setup、cleanup、setupの流れを試す副作用の問題を見つけるため
本番環境通常は1回実行通常の動作をするため

これはReactが壊れているわけではありません。むしろReactは開発中に、Effectが実行され、破棄され、もう一度実行されても安全かを確認しています。

Strict Modeで問題が出る悪い例

useEffect(() => {
  api.createUser();
}, []);

画面表示時にユーザー作成のような破壊的な処理を実行すると、開発環境で2回呼ばれてしまう可能性があります。そのため、ユーザー作成、決済、メール送信のような処理は、表示時のEffectではなく、ユーザー操作に紐づけるべきです。

良い例:ユーザー操作で実行する

const handleCreateUser = async () => {
  await api.createUser();
};

return (
  <button onClick={handleCreateUser}>
    作成
  </button>
);

つまり、画面が表示されたから作成するのではなく、ユーザーがボタンを押したから作成する。このように、処理のきっかけを自然なイベントに寄せると、Strict Modeでも壊れにくくなります。

注意点4:cleanupを書いて後片付けする

また、イベント登録やタイマーのように、開始したら終了処理が必要なものは、戻り値でcleanupを書きます。

イベント登録のcleanup

useEffect(() => {
  const handleResize = () => {
    console.log(window.innerWidth);
  };

  window.addEventListener("resize", handleResize);

  return () => {
    window.removeEventListener("resize", handleResize);
  };
}, []);

removeEventListenerを書かないと、コンポーネントが消えた後もイベントが残る可能性があります。その結果、画面遷移を繰り返すたびに同じ処理が増え、意図しない動作やメモリ消費につながります。

タイマーのcleanup

useEffect(() => {
  const timerId = setInterval(() => {
    console.log("実行");
  }, 1000);

  return () => {
    clearInterval(timerId);
  };
}, []);

clearIntervalは、コンポーネントが消える時やEffectが再実行される前に呼ばれます。したがって、タイマーを開始したら、必ず停止する処理もセットで書きます。

注意点5:API通信ではAbortControllerを検討する

例えば、検索画面のように、入力のたびにAPI通信する画面では、古いAPIレスポンスが後から返ってきて最新結果を上書きすることがあります。

R
↓
Re
↓
Rea
↓
React

このように入力されると、APIが複数回発行されます。そして、先に送ったリクエストが後から返ってくると、古い検索結果で画面が上書きされる可能性があります。

useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/search?q=${keyword}`, {
    signal: controller.signal,
  })
    .then((res) => res.json())
    .then(setResults)
    .catch((error) => {
      if (error.name !== "AbortError") {
        console.error(error);
      }
    });

  return () => {
    controller.abort();
  };
}, [keyword]);

AbortControllerを使うと、keywordが変わった時に古いリクエストを中断できます。特に検索、絞り込み、ページングなど、条件が短時間で変わる画面では重要です。

不要なuseEffectの代表例

一方で、実務で多いアンチパターンは、計算できる値をわざわざstateに入れ、Effectで同期するコードです。

悪い例:フルネームをstateで同期する

const [fullName, setFullName] = useState("");

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

良い例:レンダリング中に計算する

const fullName = `${firstName} ${lastName}`;

firstNameとlastNameから求められる値なら、stateに保存する必要はありません。つまり、レンダリング中に計算すれば十分です。

悪い例:件数をstateで同期する

const [count, setCount] = useState(0);

useEffect(() => {
  setCount(items.length);
}, [items]);

良い例:配列から直接求める

const count = items.length;

itemsが変われば再レンダリングされ、そのタイミングでcountも自然に再計算されます。つまり、EffectとsetStateを挟む必要はありません。

悪い例:合計金額をstateで同期する

const [total, setTotal] = useState(0);

useEffect(() => {
  setTotal(
    items.reduce((sum, item) => sum + item.price, 0)
  );
}, [items]);

良い例:必要な時に計算する

const total = items.reduce(
  (sum, item) => sum + item.price,
  0
);

計算コストが重い場合はuseMemoを検討します。ただし、まずはstate同期のためのEffectを減らすことが重要です。

Next.js App RouterではuseEffectが減る

Next.jsのApp Routerでは、Server Componentを使ってサーバー側でデータ取得できます。そのため、画面表示後にEffectでデータ取得する場面は以前より減ります。

Client Componentで取得する例

"use client";

export default function Page() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetchUsers().then(setUsers);
  }, []);

  return <UserList users={users} />;
}

この書き方では、ブラウザでReactが起動した後にAPI通信が始まります。そのため、初期表示でローディングが必要になり、Effectの管理も必要です。

Server Componentで取得する例

export default async function Page() {
  const users = await fetchUsers();

  return <UserList users={users} />;
}

つまり、Server Componentで取得できるデータなら、Effectを書かずにサーバー側で取得できます。Next.jsでは、サーバーデータはServer Component、入力フォームやモーダルなどのUI状態はClient Componentで扱う、と分けると整理しやすいです。

種類考え方
サーバーデータ記事一覧、商品一覧、ユーザー詳細Server Componentで取得できないか考える
UI状態モーダル、タブ、入力フォーム、ドロワーuseStateで管理する
ブラウザAPIとの同期localStorage、window、documentuseEffectを使う

useEffectが増えすぎた時の見直しポイント

なお、1つのコンポーネントにEffectが何個もある場合は、状態設計が複雑になっているサインかもしれません。

危険サイン起きている可能性見直し方
useEffectが5個以上ある責務が多すぎるコンポーネント分割やカスタムHook化を検討する
eslint-disableしている依存配列の問題を隠している依存関係を整理する
useEffect内でsetStateが多いstate同期になっている計算値に置き換える
useEffectが連鎖している実行順序に依存している元データから直接計算する
API通信が複数箇所に散らばるデータ取得の責務が曖昧Server ComponentやカスタムHookへ整理する

useEffectチェーンの悪い例

useEffect(() => {
  setB(a);
}, [a]);

useEffect(() => {
  setC(b);
}, [b]);

useEffect(() => {
  setD(c);
}, [c]);

このようなコードは、Aが変わるとBが変わり、Bが変わるとCが変わり、Cが変わるとDが変わります。結果として、何が原因で再レンダリングされているのか追いにくくなります。

本当にB、C、Dをstateとして持つ必要があるのかを確認し、元の値から計算できるならEffectを削除します。

カスタムHookへ切り出すタイミング

もちろん、Effectそのものが悪いわけではありません。処理の意味が独立しているなら、カスタムHookへ切り出すと読みやすくなります。

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => {
      setWidth(window.innerWidth);
    };

    window.addEventListener("resize", handleResize);

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  return width;
}

その結果、コンポーネント側では、以下のように意図だけを残せます。

function Header() {
  const width = useWindowWidth();

  return <div>{width}</div>;
}

そのため、コンポーネントには「画面幅を使っている」という意図が残り、イベント登録やcleanupの詳細はカスタムHook側に閉じ込められます。

useEffectを書く前のチェックリスト

  • これはReactの外側と同期する処理か?
  • 計算値として求められないか?
  • useStateだけで表現できないか?
  • 依存配列に必要な値が入っているか?
  • cleanupは必要か?
  • API通信ならAbortControllerが必要か?
  • Next.jsならServer Componentで取得できないか?

最後に、このチェックを通すだけで、不要なEffectはかなり減らせます。

よくある質問

useEffectはいつ使いますか?

API通信、イベント登録、タイマー、localStorage、document.titleの変更など、Reactの外側にあるものと同期するときに使います。一方で、計算や表示切り替えだけなら、基本的にEffectは不要です。

useEffectが2回実行されるのはバグですか?

開発環境でStrict Modeが有効な場合、Effectのsetupとcleanupを追加で試すことがあります。これは副作用の問題を見つけるための開発中の挙動です。一方で、本番環境では通常の挙動になります。

依存配列には何を入れればいいですか?

Effectの中で使っているpropsやstateは、基本的に依存配列へ入れます。また、ESLintの警告を理由なく無視すると、古い値を参照し続けるバグにつながります。

useEffectの中でsetStateしてもいいですか?

必要な場合はあります。ただし、計算結果をstateに同期するだけなら不要です。setStateしているstateを依存配列にも入れている場合は、無限ループにならないか確認してください。

Next.js App RouterでもuseEffectは必要ですか?

必要な場面はあります。ブラウザAPI、イベント登録、タイマー、ユーザー操作後のクライアント側処理ではEffectを使うことがあります。一方で、初期表示に必要なサーバーデータはServer Componentで取得できるため、データ取得目的の処理は減らせます。

まとめ:useEffectは何でも書く場所ではない

useEffectは便利ですが、何でも書く場所ではありません。本質は、Reactの外側と同期することです。

useEffectが不要なもの

  • 計算
  • 表示切り替え
  • state同士の同期
  • 配列の件数や合計金額の算出

useEffectが必要なもの

  • API通信
  • localStorage
  • イベント登録
  • タイマー
  • ブラウザAPIとの同期

良いReactコードは、Effectが多いコードではありません。必要な処理だけがあり、計算できる値はその場で計算し、サーバーデータは適切な場所で取得しているコードです。

React・Next.jsの実務スキルを伸ばしたい方は、まずEffectの使いどころを整理してみてください。依存配列、cleanup、不要な処理の見分け方を理解すると、バグの少ないコードを書きやすくなります。

IaC INP PM PMO PMP UX Webディレクター インフラエンジニア キャリアチェンジ フロントエンドエンジニア