2025.04.11
具体例で考える useEffect の cleanup 関数
はじめに
みなさんはReactでコンポーネントを実装する際にuseEffect
を使っていますか?useEffect
について調べてみると「クリーンアップ関数」という言葉を目にしたことがあるのではないでしょうか。
クリーンアップ関数とは、useEffect
内で副作用を定義した際、それを安全に解除・破棄するための関数です。本記事では、この仕組みについて、よくある具体例を交えながら解説していきます。
useEffect の基本をおさらい
ReactのuseEffect
は、コンポーネント内での**副作用(side effect)**を記述するためのフックです。副作用とは、データの取得、DOMの操作、イベントリスナーの登録など、Reactの描画とは直接関係のない処理を指します。
useEffectの基本構文
useEffectは次のような書き方をします。
useEffect(() => {
// 副作用の処理
}, [依存値]);
依存配列が空 ([]
) の場合、useEffect
はマウント時に一度だけ実行されます。
副作用の管理が必要な理由
React コンポーネントは何度も再描画される可能性があり、副作用が残り続けると以下のような問題が発生します:
・イベントリスナーが複数登録されてしまう ・タイマーが止まらず無駄な実行が続く ・通信が終了せず、リソースを消費し続ける
こうした事態を防ぐために、クリーンアップ関数の利用が必要になります。
cleanup関数とは
useEffect
の中でreturnする関数は、副作用を終了させるためのクリーンアップ関数として扱われます。
useEffect(() => {
// 副作用の開始処理
return () => {
// クリーンアップ処理
};
}, [依存値]);
この関数は、以下のタイミングで実行されます:
・コンポーネントがアンマウントされるとき
・次に同じuseEffect
が再実行される直前
クリーンアップを適切に記述することで、リソースの無駄や不具合を防ぐことができます。
具体例①:キーボードイベントの登録と解除
以下は、Enterキーが押されたときにアラートを表示する例です。
クリーンアップなしの例(バグの原因になる)
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
alert('Enterキーが押されました');
}
};
window.addEventListener('keydown', handleKeyDown);
}, []);
一見正しく動きますが、依存配列の指定を誤ると、同じイベントリスナーが何度も登録されてアラートが連続表示されるなどのバグを生みます。
クリーンアップありの正しい例
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
alert('Enterキーが押されました');
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, []);
クリーンアップ処理を追加することで、イベントリスナーが適切に解除され、同じ処理が複数回実行されることを防げます。
具体例②:タイマー処理(setInterval / setTimeout)
タイマーもまた、副作用の代表的な例です。
クリーンアップなしの例(タイマーが残り続ける)
useEffect(() => {
setInterval(() => {
console.log('1秒経過');
}, 1000);
}, []);
このコードは、コンポーネントがアンマウントされてもタイマーが止まらず、メモリリークの原因になります。
クリーンアップありの正しい例
useEffect(() => {
const id = setInterval(() => {
console.log('1秒経過');
}, 1000);
return () => {
clearInterval(id);
};
}, []);
clearInterval
を使って明示的にタイマーを解除することで、不要な処理の継続を防げます。
具体例③:Firestore のリアルタイム購読(onSnapshot)
FirestoreのonSnapshot
は、データの変更をリアルタイムで受け取れる便利な仕組みですが、クリーンアップを忘れると購読が永続してしまいます。
クリーンアップなしの例(購読が解除されない)
useEffect(() => {
const unsubscribe = onSnapshot(doc(db, 'users', 'userId123'), (snapshot) => {
console.log(snapshot.data());
});
}, []);
コンポーネントが消えても購読が解除されず、通信が継続し続けます。メモリリークにも繋がりますし、なによりデータベースのreadが走り続けるので従量課金プランだと課金額が膨らみ続けます。
クリーンアップありの正しい例
useEffect(() => {
const unsubscribe = onSnapshot(doc(db, 'users', 'userId123'), (snapshot) => {
console.log(snapshot.data());
});
return () => {
unsubscribe();
};
}, []);
unsubscribe
関数をreturnすることで、購読が不要になったタイミングで確実に解除できます。
クリーンアップ関数の注意点
いつuseEffect
が走ってほしいかは依存配列によって指定することができます。useEffect
は、依存配列内の値が変わるたびに再実行され、その直前にクリーンアップ関数が呼び出されます。依存配列が正しくないと、不要なクリーンアップや副作用の多重実行が起きることがあります。
よくあるアンチパターン
・クリーンアップを書き忘れる
→ 特に onSnapshot
, setInterval
, addEventListener
などは要注意。
・クリーンアップ関数内で条件分岐をする
→ 条件により解除されず副作用が残る可能性があるため、避けるべきです。
さいごに
まとめると、
・useEffect
のクリーンアップ関数は、「副作用の終了処理」を担う重要な機構です
・タイマー・イベントリスナー・サブスクリプションなど、開始と終了が対になる副作用では必ず記述するべきです
・正しく使えば、メモリリークや処理の多重実行を防ぎ、安定した React アプリケーション開発が可能になります
副作用の「始め方」だけでなく「終わらせ方」にも意識を向けることで、React コンポーネントの動作をより深く理解できるようになるでしょう。