参照の深淵を覗く:Reactの依存配列と「終わらないレンダリング」の正体
Reactのコードベースを読み解いていると、たまに「なぜこの `useEffect` は無限ループに陥っているのか?」あるいは「なぜ意図したタイミングで発火しないのか?」という壁にぶつかるはずだ。
初学者はそれを「Reactのバグ」と疑うが、我々アーキテクトからすれば、それはブラウザのメモリ領域で繰り広げられる「参照の等価性(Referential Equality)」という不可避な物理法則を無視した結果に過ぎない。
今日は、Reactのパフォーマンスを語る上で避けて通れない、依存配列におけるオブジェクトと配列の「参照問題」を、泥臭い実務の視点から解剖しよう。
—
1. なぜ「リテラル」が地雷になるのか
Reactの `useEffect` や `useCallback`、`useMemo` の依存配列は、JavaScriptの `Object.is` アルゴリズムで比較される。これはつまり、プリミティブな値ならともかく、オブジェクトや配列を直接渡すと、レンダリングのたびに「中身が同じでも、メモリ上のアドレスが異なる別物」として判定されることを意味する。
// アンチパターン: 毎レンダリングごとに新しいオブジェクトが生成される
useEffect(() => {
doSomething();
}, [{ id: 1 }]); // これが実行されるたびにメモリ内に新しい `{id: 1}` が作られ、
// useEffectは「変更があった」と勘違いして再実行される。
これがコンポーネントツリーの深い階層で起きれば、`useEffect` 内の非同期通信が走り続け、APIサーバーを叩き落とすか、ブラウザのCPU使用率を跳ね上げることになる。これが我々が直面する、最も初歩的かつ壊滅的なバグの正体だ。
—
2. `useMemo` による参照の安定化:賢いエンジニアの定石
オブジェクトの参照を安定させる最も標準的で強力な手段は `useMemo` だ。計算コストが高いかどうかに関わらず、「参照を固定する」という目的のためにこれを使う。
const options = useMemo(() => ({
color: ‘blue’,
limit: 10
}), []); // 依存配列を空にすることで、初回レンダリング時の参照を維持する
useEffect(() => {
// これで、optionsは毎レンダリングで同じメモリ位置を指すようになり、
// 余計な副作用は発火しなくなる
fetchData(options);
}, [options]);
ここで重要なのは、「いつ再計算すべきか」という境界線だ。もし `options` がプロパティから受け取った値に依存するなら、適切に依存配列へ含める。そうすることで、Reactのリアクティブなサイクルと、メモリの安定性を両立できる。
—
3. `useRef`:フレームワークの裏側を出し抜く禁じ手
もし、その値がレンダリング結果(UI)には影響しないが、副作用の中だけで参照したいという状況なら、`useRef` を使うのが最も洗練されたアプローチだ。
`useRef` は「書き換え可能なインスタンス変数」だ。レンダリングをトリガーすることなく、かつメモリ上のアドレスを永続的に保持できる。
const optionsRef = useRef({ color: ‘blue’ });
useEffect(() => {
// useRefの値はレンダリングのたびに生成されない
// useEffectの依存配列に含める必要もない(含めてもいいが、変わらないので意味がない)
callApi(optionsRef.current);
}, []);
この手法は、非同期処理の競合回避にも使える。「前回の値」を `useRef` に退避させておき、クリーンアップ関数で制御する。これは大規模なフォーム管理や、WebSocket接続の制御において、バグを未然に防ぐための強力な防波堤となる。
—
4. アーキテクトとしての提言:なぜ「防御的」であるべきか
多くのエンジニアが「動けばいい」と依存配列を `[]` に固定したり、逆に不要な値をすべて詰め込んだりする。しかし、真に堅牢なアプリケーションは、「情報の流れのライフサイクル」を完全に制御している。
- レンダリング負荷の削減: 不必要な `useEffect` の実行は、DOM操作や状態更新を誘発する。これは「見えないレンダリング負荷」として積み重なり、低スペックなモバイル端末でのUXを確実に腐らせる。
- 非同期の競合(Race Conditions): `useEffect` 内で非同期処理を行う際、参照が安定していないと、クリーンアップ関数が意図通りに発火せず、古いレスポンスでStateを上書きするという悲劇が起こる。これは `AbortController` との併用が必須だが、そもそも参照を正しく管理していれば、その必要性に気づくはずだ。
結論:技術は「仕組み」の上にある
Reactの依存配列における参照問題は、単なるJavaScriptの仕様の話ではない。ReactがいかにDOMとメモリを効率的に再利用するかという、フレームワークの思想そのものだ。
「とりあえず `useMemo` で囲む」のは悪くない。だが、その背後にある「なぜ今、この参照は変わらなければならないのか?」という問いを常に持ち続けてほしい。それができるエンジニアだけが、複雑な状態遷移を持つ大規模アプリケーションを、破綻させることなく運用できるのだ。
コードを書くとき、メモリの向こう側に見えるブラウザの挙動を想像せよ。そうすれば、おのずと「正しい参照管理」が見えてくるはずだ。

コメント