やあ。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` のクリーンアップは、いわば「来た時よりも美しく」の精神だ。自分が去った後にゴミを残さない。この美意識こそが、大規模なアプリケーションを長期間安定して運用するための唯一の道なんだよ。
さあ、エディタに戻って、君のコンポーネントのクリーンアップ関数を見直してみてくれ。きっと、まだ掃除できる場所が見つかるはずだ。健闘を祈る。

コメント