【実務・中級編】 イベントリスナーの登録とクリーンアップの完全網羅 – React実践ガイド

やあ。Reactの海を渡り歩いている諸君、今日もコンポーネントのライフサイクルと戦っているかな?

今回は、React開発者が避けては通れない、そして初心者が必ず一度は地獄を見る「イベントリスナーの管理」について話をしよう。`useEffect`を単なる「副作用を実行する場所」としか捉えていないなら、それは大きな間違いだ。これは「ブラウザのリソースを奪い合う戦場」でもあるんだよ。

メモリリーク? そんなのは昔話だと思っているかもしれないが、SPA(Single Page Application)の性質上、コンポーネントがマウント・アンマウントを繰り返す中で、リスナーがゴミとして溜まっていく状況は今も現場で多発している。今日は、プロとして恥ずかしくない、堅牢な実装パターンを叩き込む。

—

なぜ「クリーンアップ」が命綱なのか

`window.addEventListener` や `document.addEventListener` を使う際、君たちは必ず `removeEventListener` をセットで考えているだろうか?

Reactの `useEffect` は、コンポーネントがアンマウントされる時、あるいは依存配列が更新される直前に、その関数が返した「クリーンアップ関数」を呼び出す。ここを怠ると、コンポーネントが消えた後も、メモリ上にリスナーが残り続け、最悪の場合、存在しないコンポーネントの状態を更新しようとして「Warning: Can’t perform a React state update on an unmounted component」という忌々しいエラーを吐くことになる。

これは単なるエラーじゃない。ユーザーのブラウザのメモリを食いつぶし、アプリが次第に重くなっていく「見えない病巣」なんだ。

—

実践的実装パターン:これが現場のベストプラクティスだ

まずは、最も汎用性が高く、かつ安全な実装を見てほしい。ポイントは「関数を定義し、それを登録と削除の両方で参照し続けること」だ。

import { useEffect } from ‘react’;

const useWindowResize = () => {
useEffect(() => {
// 1. ハンドラを名前付き関数として定義する
// 無名関数をそのまま渡すと、removeEventListenerで参照できなくなるからだ
const handleResize = () => {
console.log(‘ウィンドウサイズが変わったよ:’, window.innerWidth);
};

// 2. リスナーを登録
window.addEventListener(‘resize’, handleResize);

// 3. クリーンアップ関数を返す
// Reactがコンポーネント消滅時に自動でこいつを実行してくれる
return () => {
window.removeEventListener(‘resize’, handleResize);
console.log(‘リスナーを綺麗に掃除したよ’);
};
}, []); // 空の依存配列:マウント時とアンマウント時のみ実行する
};

なぜ「名前付き関数」が重要なのか?

初心者はよく `window.addEventListener(‘resize’, () => { … })` と書いてしまう。これでは、`removeEventListener` を呼ぶときに「どの関数を削除すればいいのか」をブラウザが特定できない。メモリの中に「中身は同じだが別の場所にある関数」が溢れることになる。必ず名前を付けて、参照を固定しよう。

—

応用:依存配列と「リセット」のタイミング

もしリスナー内で最新の `state` を参照したい場合はどうする? 依存配列にその `state` を入れると、`state` が変わるたびに `remove` -> `add` が繰り返されてしまう。これはパフォーマンス的にあまりよろしくない。

そんな時は `useRef` を使って「最新の状態を常に追跡する箱」を用意するのが、シニアエンジニアの知恵だ。

import { useState, useEffect, useRef } from ‘react’;

const InteractiveComponent = () => {
const [count, setCount] = useState(0);
const countRef = useRef(count);

// countが変わるたびにrefを更新する
useEffect(() => {
countRef.current = count;
}, [count]);

useEffect(() => {
const handleKeyDown = (e) => {
// ここでは常に最新のcountを参照できる
console.log(‘現在のカウント:’, countRef.current);
};

window.addEventListener(‘keydown’, handleKeyDown);

return () => window.removeEventListener(‘keydown’, handleKeyDown);
}, []); // リスナーの登録は一度だけ
};

これなら、リスナーを何度も付け替える必要がない。メモリ効率と整合性の両立だ。

—

最後に:プロとしての心構え

最後に一つだけ伝えておきたい。フロントエンドエンジニアにとって「動く」ことは最低条件に過ぎない。「なぜ、そのタイミングで動くのか」「メモリリークをどう防ぐのか」を言語化できることが、君をただのコード書きから、信頼されるアーキテクトへと引き上げてくれる。

`useEffect` のクリーンアップは、いわば「来た時よりも美しく」の精神だ。自分が去った後にゴミを残さない。この美意識こそが、大規模なアプリケーションを長期間安定して運用するための唯一の道なんだよ。

さあ、エディタに戻って、君のコンポーネントのクリーンアップ関数を見直してみてくれ。きっと、まだ掃除できる場所が見つかるはずだ。健闘を祈る。

コメント

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