【テクニカル・上級編】 クリーンアップ関数の実行順序とタイミング – React実践ガイド

useEffectの深淵:クリーンアップ関数が奏でる「調和」のアーキテクチャ

Reactの`useEffect`を「副作用を起こす場所」としか認識していないなら、それはまだ入り口に過ぎない。上級エンジニアの諸君なら、このフックが単なるDOMの同期ツールではなく、「コンポーネントのライフサイクルという不可逆な時間の流れの中で、いかにしてメモリと状態の一貫性を守り抜くか」という壮大な防衛戦であることを理解しているはずだ。

今日は、特に軽視されがちな「クリーンアップ関数」の本質的な実行タイミングと、それが引き起こす副作用の競合について、現場で血を流しながら得た知見を共有しよう。

—

1. クリーンアップの実行タイミング:厳密な「デストラクション」

多くのエンジニアは「クリーンアップ関数はコンポーネントが消える時に走る」と信じている。半分正解だが、半分は致命的な勘違いだ。

正確には、「副作用が再実行される直前」と「アンマウント時」の2回、クリーンアップがトリガーされる。Reactのレンダリングパイプラインにおいて、クリーンアップは「次の状態へ移行するための古いリソースの解放」として機能する。

なぜこれが重要なのか

例えば、タイマーやWebSocketの購読を管理する場合、古い副作用を破棄せずに新しい副作用を生成すれば、メモリリークだけでなく、二重の通信や不整合なタイマーの暴走を招く。

useEffect(() => {
const timerId = setInterval(() => {
console.log(“Tick”);
}, 1000);

// 依存配列が変更されるたび、またはアンマウント時にここが実行される
return () => {
// 確実に古いタイマーを抹消する。これがなければリークの温床となる
clearInterval(timerId);
console.log(“Cleanup: Timer cleared”);
};
}, [intervalValue]); // 依存配列の変化がクリーンアップを呼び起こすトリガー

この「再実行の直前」にクリーンアップが走るという仕様は、Reactが「古い世界」を確実に破壊してから「新しい世界」を構築する、という鉄の意志の表れなのだ。

—

2. 非同期処理の競合:Race Conditionを制する

実務で最も恐ろしいのは、非同期処理(APIフェッチなど)の最中にコンポーネントがアンマウントされたり、依存配列が更新されたりすることだ。

もしクリーンアップ関数で「終了フラグ」を立てていない場合、「既に不要になった古いリクエストのレスポンス」が、最新のステートを上書きしてしまうという惨劇が起きる。

現場で使うべき「無視フラグ」パターン

`AbortController`が使えるならそれがベストだが、汎用的な解決策として「クリーンアップによる状態の凍結」を紹介しよう。

useEffect(() => {
let isCancelled = false; // クロージャで保持されるフラグ

const fetchData = async () => {
const data = await fetchUser(id);

// 非同期処理の完了時、既にクリーンアップ済なら何もしない
if (!isCancelled) {
setUserData(data);
}
};

fetchData();

// クリーンアップ関数でフラグを反転させる
return () => {
isCancelled = true;
};
}, [id]);

この「フラグの反転」は、非同期タスクがReactのレンダリングサイクルと同期していないことを補完する、非常に泥臭くも強力な防衛戦略だ。

—

3. レンダリング負荷とクリーンアップの副作用

クリーンアップ関数の中で「重い処理」をしてはいけない。クリーンアップは次のレンダリングの直前に走るため、そこでブロッキングな処理を行えば、フレームレートの低下や入力遅延(Jank)を誘発する。

  • やってはいけないこと: 巨大なオブジェクトの深いコピー、同期的な重い計算、同期的なLocalStorageへの大量書き込み。
  • 推奨されること: 参照の解放(null代入)、イベントリスナーの削除、タイマーの停止、キャンセルシグナルの送信。

もし複雑な破棄処理が必要なら、それをタスクキューに逃がすか、`useMemo`や`useRef`を駆使して、クリーンアップ関数自体は「軽量なトリガー」に徹させるのがアーキテクチャの鉄則だ。

—

結論:Reactは「時間」を管理するフレームワークである

クリーンアップ関数を正しく理解するということは、「このコンポーネントが存在しなくなるまでの短い時間のなかで、外部リソースとどう付き合うか」という哲学を理解することと同義だ。

Reactのコードを書くとき、常に自問してほしい。
「もし今、ユーザーが急激にページを切り替えたら、この副作用は行儀よく片付けられるか?」

この問いを突き詰めた先にあるのは、メモリが最適化され、競合バグが物理的に発生し得ない、堅牢で美しいアプリケーションだ。泥臭いライフサイクルの制御こそが、上級エンジニアと、ただ公式ドキュメントをなぞるだけのエンジニアを分かつ境界線である。

さあ、コードを開こう。君の書いた`useEffect`の中に、まだ片付けられていない「負債」は残っていないか?

コメント

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