【実務・中級編】 外部システムとの同期 – React実践ガイド

こんにちは。チームのコードレビューをしていて、一番「おっ、よくハマるポイントを分かっているね!」と嬉しくなる瞬間がどこか知っているかい? そう、`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 = ({ data }) => {
// DOM要素を指し示すRef
const canvasRef = useRef(null);
// 外部ライブラリのインスタンスを保持するRef(再レンダリングでリセットさせないため)
const chartInstanceRef = useRef(null);

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` に潜む「クリーンアップの切り忘れ」を優しく、しかし厳しく指摘してあげようじゃないか。ハッピー・コーディング!

コメント

タイトルとURLをコピーしました