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を書く前の判断基準
- Reactの外側にあるものと同期しているか
- 計算だけで済まないか
- useStateだけで表現できないか
- Next.jsならServer Componentで取得できないか
- それでも必要なら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、document | useEffectを使う |
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、不要な処理の見分け方を理解すると、バグの少ないコードを書きやすくなります。
一度カジュアル面談をしませんか?
株式会社bluenaは「高還元」と「伴走支援」を両立したSES企業です。単価の81〜86%を還元する報酬体系と、専任サポーターによる隔週1on1で、エンジニアが納得できるキャリアを実現します。
React・Next.jsの実務経験を伸ばしたい、今の案件やキャリアを整理したいという段階でも大丈夫です。まとまっていなくてもOKですので、まずは現在地を聞かせてください。
カジュアル面談ですので、お気軽にお聞かせください。





