お疲れ。最近、PR(プルリクエスト)のレビューをしていて「あ、これやっちゃったな」というコードを本当によく見かける。
画面がフリーズする、ファンが爆音で回り出す、そしてブラウザがクラッシュする――原因の多くは、大体こいつだ。`useEffect`の依存配列ミスによる、無限ループ地獄。
中級へのステップを駆け上がっている君たちなら、`useEffect`の基本的な使い方はもうマスターしているはずだ。だが、「なぜこのコードが無限ループを引き起こすのか」「ブラウザの裏側で何が起きているのか」、そしてそれを「どうやってスマートにデバッグし、ねじ伏せるのか」をロジカルに説明できるだろうか?
今日は、現場のシニアとして、この「無限ループという名の魔物」の正体と、そのスマートな退治方法を徹底的に叩き込んでやろう。
—
1. なぜ無限ループは起きるのか?(メカニズムの解剖)
まずは敵を知ることから始めよう。無限ループの基本方程式は極めてシンプルだ。
> 「副作用の中で状態を更新する = その状態を依存配列に入れる = 再レンダリングが走る = 再び`useEffect`が実行される」
このループの何が厄介かというと、Reactのライフサイクルとブラウザのレンダリングパイプラインが密接に関係している点にある。裏側で何が起きているか、時系列で追ってみよう。
1. 初期レンダリング(Mount): コンポーネントが画面に描画される。
2. 副作用の実行: `useEffect`の中身が走る。ここで `setCount(count + 1)` のような状態更新が呼ばれる。
3. 状態の変更と再レンダリングの予約: Reactが「ステート変わったから、もう一回コンポーネント関数を実行し直さなきゃ」と検知する。
4. コミットとペイント: ブラウザがDOMを更新し、画面に反映する(ここですでにCPU使用率が跳ね上がる)。
5. 依存配列の評価: 再レンダリング後、Reactは`useEffect`の依存配列を前回の値と比較する。「おっ、`count`の値が変わってるじゃん!」
6. 副作用の再実行: 依存配列が変わったため、再び`useEffect`がトリガーされる。→ 2に戻る(無限ループ突入)
ブラウザのメインスレッドはJavaScriptの実行と画面描画を同じスレッドで行っている。ここで無限ループが発生すると、UIの入力やアニメーション処理が一切できなくなり、ブラウザは完全にフリーズする。これが「やらかした」瞬間の裏側の舞台裏だ。
—
2. 実務でよくある「やっちまった」コード例
百聞は一見にしかず。実務でありがちな、オブジェクトや配列を依存配列に入れて無限ループを引き起こすアンチパターンを見てみよう。APIからデータを取得して、ローカルの状態にマージしようとした時によくあるミスだ。
import React, { useState, useEffect } from ‘react’;
export const UserProfileCard = () => {
const [user, setUser] = useState({ name: ‘Yamada’, score: 100 });
const [fetchCount, setFetchCount] = useState(0);
// 【アンチパターン】オブジェクトを依存配列に直接入れている例
useEffect(() => {
console.log(‘副作用が走りました’);
// 何らかの処理でオブジェクトを新しく作り直してセットしているとする
// (実際はAPIコールやデータ加工のつもり)
setUser({
name: ‘Yamada’,
score: user.score + 1, // 現在のスコアに依存している
});
setFetchCount(prev => prev + 1); // 回数カウント
}, [user]); // ← ここが諸悪の根源!
return (
名前: {user.name}
スコア: {user.score}
フェッチ回数: {fetchCount}
);
};
このコードをそのまま動かすと、`user`オブジェクトが更新されるたびに新しい参照(Reference)が生まれ、依存配列の比較(Shallow Equal / 浅い比較)によって「値が変わった!」と判定され、無限ループの爆弾が爆発する。
—
3. React DevToolsを使った「秒速」デバッグ手法
もし君が書いたコードでタブが固まったら、慌さずにReact DevToolsを起動しよう。コンソールに無限ログが流れてパニックになる前に、的確に病巣を特定する手順はこうだ。
手順1: Componentsタブで「レンダリングのハイライト」を使う
1. ブラウザの「React DevTools」を開き、歯車マーク(Settings)をクリック。
2. 「Highlight updates when components render」(コンポーネントが再描画されたときにハイライトする)にチェックを入れる。
3. 問題のコンポーネントを表示した瞬間、画面が激しく赤や緑で点滅し始めるはずだ。これで「どのコンポーネントが狂ったように再描画されているか」が一発で特定できる。
手順2: Profilerタブでコミットの嵐を確認する
1. React DevToolsの「Profiler」タブに切り替え、録画ボタン(●)を押す。
2. 数秒間アプリを操作(またはフリーズする様子を再現)し、録画を停止する。
3. グラフ上に「Flamegraph」が表示される。ここで、「たった数秒の間に何百回もコミット(Render)が走っている」という異常事態を視覚的に確認できる。どれか一つのコンポーネントが異常な回数レンダリングされていれば、そこが犯人だ。
—
4. 現場で使える!正しい処方箋とベストプラクティス
原因が分かったところで、これをどう直すか。プロのエンジニアとしての綺麗な処方箋を提示しよう。
処方箋A: プリミティブな値に分解する、または関数型アップデートを使う
オブジェクト全体を依存配列に入れるのではなく、必要なプリミティブ値(この場合は`user.score`など)に絞るか、あるいは「そもそも`useEffect`の中で状態を更新する必要があるのか?」を疑うべきだ。
大抵の場合、状態の派生値(Derived State)は `useEffect` で同期する必要はない。レンダー中に計算するか、イベントハンドラ内で完結させるべきだ。
処方箋B: 依存配列の設計を見直した正しい実装例
先ほどのユーザープロファイルの例を、無駄な副作用を使わずに正しくリファクタリングしたコードがこれだ。
import React, { useState, useEffect, useCallback } from ‘react’;
export const UserProfileCardFixed = () => {
const [user, setUser] = useState({ name: ‘Yamada’, score: 100 });
const [fetchCount, setFetchCount] = useState(0);
// 【ベストプラクティス】
// 副作用は「外部システムとの同期(API取得や購読など)」に限定する。
// 今回のようにマウント時に一度だけデータを取ってくるケースを想定。
// データを取得する関数を useCallback でメモ化し、依存関係を明確にする
const fetchUserData = useCallback(() => {
// ここでAPIリクエストを行っていると仮定
console.log(‘APIからデータを取得しています…’);
// 仮のデータ取得成功時の処理
setUser(prevUser => ({
…prevUser,
score: prevUser.score + 10,
}));
}, []); // 外部の変数に依存しないため、依存配列は空
useEffect(() => {
// マウント時に一度だけ実行されるべき処理
fetchUserData();
setFetchCount(prev => prev + 1);
// クリーンアップ関数が必要な場合はここに書く
return () => {
console.log(‘コンポーネントがアンマウントされました’);
};
}, [fetchUserData]); // 依存配列にはメモ化された関数を入れるため、無限ループしない
return (
ユーザープロフィール(修正版)
名前: {user.name}
スコア: {user.score}
データ取得回数: {fetchCount}
);
};
—
5. シニアからのプロの心得
`useEffect` を書くとき、自分に対して常にこう問いかける習慣をつけてほしい。
> 「この副作用、本当に `useEffect` じゃなきゃダメか?」
- ユーザーのボタンクリックや入力による状態更新なら、イベントハンドラ(Event Handlers)に書くべきだ。
- プロップスや他のステートから計算できる値なら、わざわざステートを持たせず、その場で計算(Derived State)すればいい。
- `useEffect` はあくまで、Reactの管理外である「外部システム(DOM、Web API、WebSocket、タイマーなど)との同期」をとるための最終手段だ。
この意識を持つだけで、君の書くコードから無限ループのバグは劇的に減る。そして、万が一バグを踏んでも、今日紹介したDevToolsの技を使えば秒速で解決できるはずだ。
さあ、エディタに戻って、自分のコードの依存配列をもう一度見直してみようか。頼んだぞ!

コメント