アンマウント時の処理(クリーンアップ関数)の書き方
生徒
「コンポーネントが画面から消えるときにやることって何ですか?どう書けば安全ですか?」
先生
「コンポーネントが消えるときに不要な処理を止めたり後片付けをするのがクリーンアップです。useEffectの返り値に関数を書くだけで実行できますよ。」
生徒
「実際の例を見ながら教えてください!」
先生
「もちろんです。初めに重要な考え方から説明して、そのあとで具体的なコード例を紹介します。」
1. クリーンアップ関数とは?なぜ必要か
クリーンアップ関数とは、コンポーネントがアンマウントされる直前や依存が変わる直前に呼ばれる後片付け用の関数です。
例えばタイマー、イベントリスナー、WebSocket、購読(subscription)、非同期処理などを登録したら、反対に解除や停止をしないとメモリが解放されなかったり、アンマウント後に状態を更新してエラーになることがあります。これを防ぐためにクリーンアップを書きます。
2. useEffectでの基本的な書き方
関数コンポーネントではuseEffectの中で副作用を始め、関数を返すことでクリーンアップを定義します。最も基本的な形は次の通りです。
import React, { useEffect } from "react";
function Example() {
useEffect(() => {
// ここで副作用を開始する(イベント登録やAPI呼び出しなど)
return () => {
// ここでクリーンアップ(解除や停止)を行う
};
}, []); // 依存配列を指定
return <div>動作例</div>;
}
export default Example;
3. タイマーの例:clearIntervalで停止する
setIntervalやsetTimeoutを使う場合は、IDを保存してクリーンアップでクリアします。
import React, { useEffect, useState, useRef } from "react";
function Timer() {
const [count, setCount] = useState(0);
const timerRef = useRef(null);
useEffect(() => {
timerRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => {
clearInterval(timerRef.current);
};
}, []);
return <div>経過秒数: {count}</div>;
}
export default Timer;
4. イベントリスナーの例:removeEventListenerで解除
ウィンドウやDOMにイベントを登録したら必ず解除します。追加と解除は対になっていることを意識してください。
import React, { useEffect } from "react";
function ResizeWatcher() {
useEffect(() => {
function onResize() {
console.log("ウィンドウサイズが変わりました");
}
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
};
}, []);
return <div>ウィンドウの変更を監視中</div>;
}
export default ResizeWatcher;
5. 非同期処理のキャンセル:AbortControllerを使う
fetch等の非同期処理はキャンセル可能にしておくと便利です。AbortControllerを使えばアンマウント時にリクエストを中止できます。
import React, { useEffect, useState } from "react";
function FetchData({ id }) {
const [data, setData] = useState(null);
useEffect(() => {
const ac = new AbortController();
fetch(`/api/item/${id}`, { signal: ac.signal })
.then(r => r.json())
.then(setData)
.catch(err => {
if (err.name !== "AbortError") console.error(err);
});
return () => ac.abort();
}, [id]);
return <div>{data ? "読み込み完了" : "読み込み中"}</div>;
}
export default FetchData;
6. 購読やソケットの解除
WebSocketやEventSource、外部ライブラリの購読は必ず解除します。解除方法はライブラリによって異なりますが、useEffectのクリーンアップで必ず呼ぶようにします。
useEffect(() => {
const ws = new WebSocket("wss://example");
ws.addEventListener("message", onMessage);
return () => {
ws.removeEventListener("message", onMessage);
ws.close();
};
}, []);
7. クリーンアップの設計上の注意点
- クリーンアップは副作用を起こした逆の操作を行うこと
- 依存配列を正しく指定して、不要な再登録や解除を避けること
- 非同期処理は途中でキャンセルできるように実装すること
- 参照型(オブジェクトや関数)を依存に含めると再生成でクリーンアップが頻発するため、必要ならuseCallbackやuseRefで安定化する
8. 実践的なデバッグヒント
開発中はコンソールでマウントとアンマウントのログを出すとクリーンアップが正しく動いているか確認できます。また開発ツールのネットワークやメモリプロファイラで未解放のリソースがないかチェックしましょう。
9. よくある間違いと対処法
- クリーンアップを書き忘れてアンマウント後のsetStateで警告が出る → クリーンアップを追加する
- 依存を空にしてしまい期待通りに再実行されない → 正しい依存を指定する
- 複数の副作用を一つのuseEffectにまとめすぎて読みにくくなる → 副作用ごとにuseEffectを分ける
最後に
クリーンアップは安全で健全なReactアプリを作るための必須テクニックです。副作用を始めたら必ず解除する習慣をつけると、バグやメモリリークを未然に防げます。まずはタイマー、イベント、fetchから実践してみてください。
まとめ
React開発において、コンポーネントのライフサイクルを管理することは、パフォーマンスの向上やバグの抑制に直結する極めて重要なスキルです。特に今回学習した「アンマウント時の処理(クリーンアップ関数)」は、目に見えない部分でのメモリリークを防ぎ、アプリケーションの安定性を支える屋台骨となります。
クリーンアップ関数の役割とメリットの再確認
ReactのuseEffectフックは、マウント時や更新時に実行される「副作用」を記述するためのものですが、その戻り値として関数を指定することで「後片付け」を自動化できます。この仕組みがあるおかげで、開発者は命令的な命令を最小限に抑えつつ、宣言的にリソースの管理を行うことが可能になります。
- メモリリークの防止: 不要になったタイマーやリスナーを破棄することで、ブラウザのメモリ消費を最適化します。
- 予期せぬエラーの回避: 既に存在しないコンポーネントに対して状態更新(setState)を行おうとする際に出る警告を防ぎます。
- リソースの節約: 不要なネットワーク接続(WebSocket)やAPIリクエストを中断することで、サーバー負荷と通信量を削減します。
実践的な応用:複数リソースを扱うカスタムフックの例
実際のプロジェクトでは、クリーンアップ処理をより再利用しやすくするために、カスタムフックの中にカプセル化することが一般的です。以下のサンプルプログラムは、画面のスクロール位置を監視しつつ、コンポーネントが消える際に確実にリスナーを削除するカスタムフックの構成例です。
import React, { useState, useEffect } from "react";
// スクロール位置を監視するカスタムフック
function useScrollPosition() {
const [scrollPosition, setScrollPosition] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollPosition(window.scrollY);
};
// イベントリスナーの登録
window.addEventListener("scroll", handleScroll);
// クリーンアップ関数(アンマウント時に実行される)
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return scrollPosition;
}
// 実際にコンポーネントで使用する例
function ScrollDisplay() {
const position = useScrollPosition();
return (
<div style={{ height: "200vh", padding: "20px" }}>
<div style={{ position: "fixed", top: 10, left: 10, background: "#fff", border: "1px solid #ccc", padding: "10px" }}>
現在のスクロール位置: {position}px
</div>
</div>
);
}
export default ScrollDisplay;
開発効率を高めるためのベストプラクティス
クリーンアップ関数を書く際には、単に「消去する」だけでなく、一貫性を持たせることが大切です。例えば、addEventListenerを使ったら必ずremoveEventListenerを使う、setTimeoutを使ったらclearTimeoutを呼ぶといった「セットでの思考」を習慣化しましょう。
また、最新のReact(特にStrict Modeが有効な開発環境)では、マウントとアンマウントが意図的に2回繰り返されることがあります。これは、クリーンアップ処理が正しく実装されているかを検証するための仕様です。もし2回実行されて挙動がおかしくなる場合は、クリーンアップ関数が不足しているサインですので、コードを見直す良いきっかけになります。
さらなるステップアップに向けて
クリーンアップの基本をマスターしたら、次は「依存配列(Dependency Array)」との関係性をさらに深く掘り下げてみてください。特定の変数が変わるたびにクリーンアップが走り、新しい副作用がセットされるというサイクルを理解することで、より高度なデータフェッチやリアルタイム通信の実装が可能になります。
Reactは非常に強力なツールですが、その力を最大限に引き出すためには、こうした地味ながらも重要な「後片付け」の技術が欠かせません。綺麗なコードは、綺麗な後片付けから始まると言っても過言ではないでしょう。
生徒
先生、ありがとうございました!クリーンアップ関数って、単に「終わらせる」だけじゃなくて、次の処理をスムーズに始めるための準備でもあるんですね。
先生
その通りです!よく気づきましたね。Reactのコンポーネントは、再レンダリングによって何度も「生まれては消えて」を繰り返します。そのサイクルの中で古い情報を引きずらないように掃除するのが、クリーンアップの本当の役割なんですよ。
生徒
さっきのスクロールの例も分かりやすかったです。もしremoveEventListenerを忘れてしまったら、画面を切り替えるたびにリスナーがどんどん増えていって、ブラウザが重くなってしまうってことですよね?
先生
正解です。それを放置すると「メモリリーク」という現象が起きて、最終的にはブラウザがクラッシュすることもあります。特に複雑なWebアプリになればなるほど、この小さな一行のreturnが大きな差を生むんです。
生徒
「副作用を始めたら、必ず対になる解除処理を書く」というルールを徹底します。あと、開発モードで2回ログが出る理由も分かりました。あれはReactが「ちゃんと後片付けできてるかチェックしてあげるよ!」って教えてくれてたんですね。
先生
素晴らしい理解度ですね。React 18以降は特にその傾向が強いので、ログを味方につけてデバッグしてみてください。タイマーやAPIの取得など、今回学んだパターンを自分のプロジェクトでもぜひ試してみてね。
生徒
はい!まずは一番簡単なsetTimeoutのクリアから始めて、徐々にAbortControllerを使った高度なキャンセル処理にも挑戦してみます。コードがスッキリして、動作も軽くなるのが楽しみです!
先生
その意気です。もし途中で分からなくなったら、またいつでも聞いてください。次は、より効率的なデータの管理方法についても一緒に学んでいきましょう。