- JavaScript
- 技術 💻
ReactのuseEffectの使い方|依存配列と使わなくてよい場面
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期連続の黒字決算。