useEffectの「無限ループ地獄」から脱出せよ:オブジェクト・配列の参照比較という落とし穴
現場でコードレビューをしていると、中級レベルに差し掛かったエンジニアがよく頭を抱える問題がある。「`useEffect`が意図せず無限ループする」「なぜか値が変わっていないのにエフェクトが走る」というやつだ。
これの犯人は、9割方「依存配列におけるオブジェクト・配列の参照比較(Referential Equality)」の理解不足だ。今日は、Reactが裏側でどうやって依存関係を監視しているのか、そして僕たちがどうやってその「不機嫌なレンダリング」を制御すべきか、現場の視点で叩き込んでいく。
—
なぜReactは「値の内容」を見ないのか?
Reactの依存配列(`[dep]`)は、JavaScriptの `Object.is` アルゴリズムを使って比較を行っている。これは厳密等価演算子 `===` とほぼ同じだ。
ここが重要なんだが、JavaScriptにおいてオブジェクトや配列は「参照型」だ。
const obj1 = { id: 1 };
const obj2 = { id: 1 };
console.log(obj1 === obj2); // false
コンポーネントが再レンダリングされるたびに、関数コンポーネント内の変数は再定義される。つまり、メモリ上の「新しい場所」に同じ中身のオブジェクトが生成される。Reactは「おっ、前のオブジェクトとメモリ番地が違うな。よし、副作用を実行するか!」と判断してしまい、それがまた再レンダリングを誘発する……これが無限ループの正体だ。
—
実践:参照を安定化させる「3つの武器」
この問題を回避するための実務的なテクニックを、優先度順に伝授する。
1. `useMemo` でオブジェクトの参照を固定する
一番よく使う手だ。計算コストが高いロジックだけでなく、単に「参照を維持したいだけ」のオブジェクトにも有効だ。
// 依存配列に直接オブジェクトを置くと、毎レンダリングで再実行される
const options = useMemo(() => ({
color: ‘blue’,
threshold: 0.5
}), []); // 依存配列を空にすれば、マウント時のみ生成される
useEffect(() => {
// これで、optionsがメモリ上で安定するため、エフェクトは初回のみ走る
doSomething(options);
}, [options]);
2. `useRef` で「監視不要な値」を逃がす
もしその値がレンダリング結果(JSX)に直接影響しないのであれば、`useRef` に避難させるのが一番賢い。`useRef` は書き換えても再レンダリングをトリガーしないし、参照先は常に同じメモリ番地を指し続ける。
const optionsRef = useRef({ color: ‘blue’ });
useEffect(() => {
// optionsRef.current は常に同じ参照なので、依存配列に書く必要すらない
const currentOptions = optionsRef.current;
// 何か処理をする
}, []);
3. プリミティブな値に分解する(究極の防衛策)
そもそも「オブジェクト全体」を依存配列に入れるのが間違いというケースも多い。必要なプロパティだけを個別に抽出して渡せば、Reactの比較エンジンは平和を保てる。
// 悪い例: { id: 1, name: ‘react’ } 全体を渡すと、nameが空文字でもidが変われば再実行される
// 良い例: 必要なものだけを依存させる
useEffect(() => {
console.log(‘IDが変わった時だけ実行される’);
}, [user.id]);
—
現場で「やってはいけない」アンチパターン
たまに、以下のようなコードを書いて「動いたからOK」としている人を見かけるが、これは爆弾を埋め込んでいるのと同義だ。
- 依存配列を空にする(`[]`): 「とりあえず動かしたい」という理由で、本当は依存すべき値を無視するのは避けること。将来的にバグの温床になる。
- eslint-plugin-react-hooks の警告を無視する: `react-hooks/exhaustive-deps` の警告は、君を苦しめるためのものではなく、Reactの作法を守らせるための命綱だ。これを無視して `// eslint-disable-line` を貼るのは、コードの治安を悪くする行為だぞ。
—
まとめ:アーキテクトからのアドバイス
いいか、Reactの再レンダリング制御は「いかに無駄な計算を省くか」という引き算の美学だ。
1. オブジェクトの参照は、必ず変わるものだと思え。
2. `useMemo` や `useRef` で「安定した参照」を供給してやる。
3. 依存配列には「本当に変化した時だけ再実行したい値」だけを置く。
この感覚が身につくと、Reactアプリのパフォーマンスは劇的に安定する。魔法のような解決策を探すより、JavaScriptの基本である「参照」の概念をコードに落とし込むこと。それが、一流のエンジニアへの近道だ。
さて、今日学んだことを今のプロジェクトで試してみてくれ。もし「このケースはどうすれば?」という悩みがあれば、いつでも相談に乗るよ。現場からは以上だ。

コメント