こんにちは。チームのコードレビューをしていて、一番「おっ、よくハマるポイントを分かっているね!」と嬉しくなる瞬間がどこか知っているかい? そう、`useEffect`を使った外部システムとの同期処理、そしてそのクリーンアップが綺麗に書かれている時なんだ。
中級への階段を登るエンジニアたちが、よくここで躓く。「なんかレンダリングのたびにAPIが二重に叩かれるんです」「画面を離れてもリスナーが生き残ってメモリリークしてます」――うん、よくある話だ。
今回は、Reactの管理外にある「野生の王国(=外部システムやDOM API)」と、私たちの愛するReactのステートを安全に同期させるための作法について、現場の泥臭い知見を交えながら徹底的に解説しよう。
—
1. そもそも「外部システムとの同期」って何をやっているのか?
Reactのコンポーネントは、本来「状態(State)という入力から、UI(JSX)という出力を純粋に計算する関数」であるべきだ。これが理想郷。
しかし、現実はどうだろう?
- Google Mapsなどのサードパーティ製インタラクティブマップを埋め込みたい。
- WebSocketサーバーと常時接続してリアルタイムにデータを同期したい。
- ウィンドウのresizeイベントや、グローバルなキーボード入力を監視したい。
これらはすべて、Reactのライフサイクルの外側で勝手に生きている「外部システム」だ。Reactの世界(内部)と、ブラウザやライブラリの世界(外部)の通訳をさせるためにこそ、`useEffect`という強力なフックが用意されている。
ここで重要なマインドセットを一つ。
> 「`useEffect`はライフサイクルメソッド(`componentDidMount`など)の代替ではない。同期のためのブリッジ(橋渡し)である」
この視点を持てるかどうかが、君を真のシニアへと引き上げる分水嶺になる。
—
2. ブラウザの裏側で何が起きているか?(実行のタイミング)
Reactが画面を描画する時、ブラウザの裏側では次のようなドラマが繰り広げられている。
1. Render Phase(描画の計算): コンポーネントが実行され、JSXが返される。
2. Paint(画面への反映): ブラウザがDOMを実際に画面にピクセルとして描き出す。(※ユーザーにはこの時点で画面が見えている)
3. Effect Phase(副作用の実行): 画面の描画が完了した後に、非同期で`useEffect`の中身が実行される。
なぜ画面描画の後なのか? それは、DOMの操作や重い外部ライブラリの初期化を先やると、ユーザーが画面を表示するまでの体感速度(TBTなど)が落ちてしまうからだ。Reactは「ユーザーへの描画を最優先し、そのあと裏でこっそり同期をとる」という優しさを持っている。
そして、再レンダリングが走る時、あるいはコンポーネントがアンマウント(画面から消滅)する時には、次の順序で処理が走る。
1. 前回のクリーンアップ関数の実行(古いStateやPropsを元にした副作用の片付け)
2. 新しい副作用の実行
この「片付けてから新しいものを繋ぐ」というライフサイクルを完全にコントロールするのが、クリーンアップ関数の役割というわけだ。
—
3. 実践!ウィンドウサイズを安全に同期するカスタムフック
理屈はこれくらいにして、現場で即座に使える綺麗なコードを見ていこう。
今回は、ウィンドウのリサイズイベントをReactのStateと同期させる、極めて実用的な例だ。メモリリークの対策も完璧にしてある。
import { useState, useEffect } from ‘react’;
// ウィンドウの幅と高さを管理するカスタムフック
export function useWindowSize() {
// 1. ReactのStateとして外部システムの値を保持する
const [windowSize, setWindowSize] = useState({
width: typeof window !== ‘undefined’ ? window.innerWidth : 0,
height: typeof window !== ‘undefined’ ? window.innerHeight : 0,
});
useEffect(() => {
// 2. 外部システム(window)の変更を検知するハンドラー
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
// 初回マウント時にも正確なサイズを取得して同期させる
handleResize();
// 3. 外部システムへのイベントリスナーの登録(同期の開始)
window.addEventListener(‘resize’, handleResize);
// 4. 【超重要】クリーンアップ関数
// コンポーネントが消える時、または再登録の直前に古いリスナーを必ず剥がす
return () => {
window.removeEventListener(‘resize’, handleResize);
};
}, []); // 依存配列が空 = マウント時に一度だけ同期を確立し、アンマウント時に片付ける
return windowSize;
}
なぜこのコードが美しいのか?(シニアからの解説)
1. クリーンアップの徹底: `window.removeEventListener`を忘れるエンジニアが後を絶たない。これを忘れると、コンポーネントが画面から消えてもイベントリスナーがメモリ上に残り続け、メモリリークや予期せぬバグを引き起こす。クリーンアップは「お掃除」ではなく「契約」だと思ってほしい。
2. SSR(サーバーサイドレンダリング)への配慮: `typeof window !== ‘undefined’` のチェックを入れている。Next.jsなどの環境で、サーバー側(Node.js環境)でレンダリングされた時に`window`が存在せずに爆発するのを防ぐ、プロの防衛策だ。
—
4. 応用編:外部ライブラリ(Chart.js等)のDOM同期と破棄
もう少し実践的な例を挙げよう。キャンバスにグラフを描画するような外部ライブラリをReactで使う場合だ。ライブラリのインスタンスは「外部システム」そのものなので、しっかりとライフサイクルを同期させる必要がある。
import React, { useEffect, useRef } from ‘react’;
// 仮の外部チャートライブラリを想定
import { createChartInstance } from ‘some-heavy-chart-library’;
interface ChartProps {
data: number[];
}
export const ReactiveChart: React.FC
// DOM要素を指し示すRef
const canvasRef = useRef
// 外部ライブラリのインスタンスを保持するRef(再レンダリングでリセットさせないため)
const chartInstanceRef = useRef
useEffect(() => {
// DOMがマウントされていなければ何もしない
if (!canvasRef.current) return;
// 1. 外部ライブラリのインスタンスを生成してDOMと同期
chartInstanceRef.current = createChartInstance(canvasRef.current, { data });
// 2. クリーンアップ:コンポーネントが消える、またはデータが激変する前にチャートを破壊する
return () => {
if (chartInstanceRef.current) {
chartInstanceRef.current.destroy();
chartInstanceRef.current = null;
}
};
}, [data]); // dataが更新されるたびに、一度チャートを壊して新しく作り直す(堅実な同期パターン)
return (
);
};
ここが現場の勘所
外部ライブラリのインスタンスを保持するのに `useRef` を使っている点に注目してほしい。Stateを使う必要はない。なぜなら、インスタンスの変更自体は「画面の再描画(再UI構築)」をトリガーすべきではないからだ。
「UIの再描画を引き起こさないけれど、コンポーネントのライフサイクルを通じて保持したい値」には `useRef`。これ、テストでも絶対出るから覚えておいて損はないよ。
—
5. まとめ:シニアからのメッセージ
`useEffect` を用いた外部システムとの同期は、一見すると魔術のように見えるかもしれない。しかし、以下の3つのルールを心に刻んでおけば、もう怖くないはずだ。
1. Reactの世界と外の世界の「橋渡し」だと意識する。
2. イベントの登録や接続をしたら、必ずセットでクリーンアップ関数で「後始末」を書く。
3. 同期させる必要のない値(再描画が不要なもの)は `useRef` に逃がす。
Reactは、私たちが正しくルールを守りさえすれば、非常に頑健でパフォーマンスの高いアプリケーションをプレゼントしてくれる。
さあ、今日のコードレビューでは、後輩たちの `useEffect` に潜む「クリーンアップの切り忘れ」を優しく、しかし厳しく指摘してあげようじゃないか。ハッピー・コーディング!

コメント