useEffectでクリーンアップ関数を使う方法をやさしく解説
生徒
「先生、useEffectの中でやった処理を後で片付ける方法ってありますか?」
先生
「ありますよ。useEffectはクリーンアップ関数を返すことで登録したイベントやタイマーを解除したり、リソースを解放したりできます。」
生徒
「クリーンアップって具体的にどういうことをするんですか?」
先生
「例えばイベントリスナーの解除やタイマーの停止、非同期処理のキャンセルなどです。実例を見ながら説明しましょう。」
1. クリーンアップ関数とは?わかりやすい説明
クリーンアップ関数とは、useEffectの中で「後片付けのために返す関数」のことです。Reactでは画面が表示されたり消えたり、何度も切り替わります。そのときに、開始した処理をそのまま放置してしまうと、動かなくなった画面の裏側で処理だけが動き続けてしまいます。これを防ぐために、useEffectでは「始めた処理には、必ず終わらせる処理を書く」という考え方がとても大切になります。
たとえば、スイッチを入れたら最後に必ず電源を切る、蛇口をひねったら使い終わったら水を止める、といったイメージです。クリーンアップ関数を書くことで、アプリの動作が安定し、メモリの無駄遣いや思わぬ不具合を防ぐことができます。
useEffect(() => {
console.log("画面が表示されました");
return () => {
console.log("画面が消える前に後片付けします");
};
}, []);
このように、useEffectの中で処理を書き、returnで返した関数がクリーンアップ関数になります。難しく考えず、「useEffectで何かをしたら、最後に元に戻す処理を書く」と覚えておくと、初心者の方でも理解しやすくなります。
2. 基本的な書き方
useEffectは、Reactの中で「画面が表示されたときに何かをする」「画面が切り替わる前に後片付けをする」といった流れをまとめて書ける仕組みです。基本的には、useEffectの中で最初に処理を書き、その最後にreturnでクリーンアップ関数を返します。この形を覚えておくだけでも、React初心者の方はかなり理解しやすくなります。
特に大切なのは、「最初の関数が実行される」「returnで返した関数が後で呼ばれる」という順番です。returnの中身はすぐには実行されず、コンポーネントが消えるときや、設定した条件が変わったときに呼び出されます。そのため、ここには元に戻す処理や終了処理を書くのが基本になります。
useEffect(() => {
// 画面が表示されたときに行う処理
console.log("処理を開始しました");
return () => {
// 画面が切り替わる前や消える前に行う処理
console.log("処理を終了します");
};
}, []);
また、最後の配列は「依存配列」と呼ばれ、いつこの処理を動かすかを決める重要な部分です。空の配列を指定すると、画面が最初に表示されたときと、消えるときだけ処理が実行されます。今は「useEffectは、処理と後片付けをセットで書くもの」と覚えておけば十分です。
3. 例①:イベントリスナーの登録と解除
useEffectのクリーンアップが特に分かりやすい例が、イベントリスナーの登録と解除です。イベントリスナーとは、キーボード操作やマウス操作など、ユーザーの行動をきっかけに処理を実行する仕組みです。画面が表示されている間だけ反応させ、不要になったら確実に解除することが重要になります。
たとえば「キーが押されたら何かを表示する」といった処理を考えてみましょう。画面が切り替わったあともイベントが残っていると、見えない画面に対して処理が走り続けてしまい、思わぬ不具合の原因になります。そのため、useEffectとクリーンアップ関数をセットで使います。
useEffect(() => {
const handler = (e) => {
console.log(e.key);
};
window.addEventListener("keydown", handler);
return () => {
window.removeEventListener("keydown", handler);
};
}, []);
ここでのポイントは、登録するときと解除するときに「同じ関数」を使うことです。無名関数を直接書いてしまうと、解除時に別の関数と判断され、正しくリスナーが外れません。初心者の方は「関数を変数に入れてから登録し、その変数で解除する」と覚えておくと安心です。
4. 例②:タイマーの開始と停止
タイマー処理も、useEffectのクリーンアップがとても重要になる代表的な例です。setIntervalやsetTimeoutは、一度動き始めると明示的に止めない限り、画面が切り替わっても動き続けます。そのため、コンポーネントが不要になったタイミングで、必ずタイマーを停止する必要があります。
たとえば「一定時間ごとにメッセージを表示する」といった処理を考えてみましょう。画面を移動したあともタイマーが残っていると、見えないところで処理が動き続け、無駄な負荷や予期しない動作につながります。こうした問題を防ぐために、クリーンアップ関数でタイマーを止めます。
useEffect(() => {
const id = setInterval(() => {
console.log("tick");
}, 1000);
return () => {
clearInterval(id);
};
}, []);
このように、setIntervalで受け取ったIDを変数に保存し、クリーンアップ関数でclearIntervalを呼び出します。「タイマーを開始したら、必ず停止する処理を書く」という流れを意識しておくと、初心者の方でも安全にuseEffectを使えるようになります。
5. 例③:非同期処理のキャンセル
fetchなどの非同期処理はコンポーネントがアンマウントされた後に結果を取り扱うとエラーになることがあります。クリーンアップでキャンセルする方法はいくつかあります。
useEffect(() => {
let cancelled = false;
fetch("/api/data")
.then(r => r.json())
.then(data => {
if (!cancelled) setData(data);
});
return () => { cancelled = true; };
}, []);
よりモダンな方法としてAbortControllerを使うと、フェッチ自体を中断できます。
useEffect(() => {
const ac = new AbortController();
fetch("/api/data", { signal: ac.signal })
.then(r => r.json())
.then(data => setData(data))
.catch(err => {
if (err.name !== "AbortError") console.error(err);
});
return () => ac.abort();
}, []);
6. 再レンダー時のクリーンアップの挙動
依存配列に値が入っている場合、その値が変わるとReactはまず前の副作用のクリーンアップ関数を実行し、その後で新しい副作用を実行します。これにより二重登録や古い状態のまま処理を続けることを防げます。
7. クリーンアップ忘れで起きる問題と対処法
クリーンアップを忘れると次のような問題が発生します。メモリリーク、同じ処理が複数回実行される、アンマウント後に状態更新してReactの警告が出るなど。対処法は必ずクリーンアップ関数を返すこと、登録した関数参照を保存して同じものを解除すること、非同期はキャンセル可能にすることです。
8. Strict Modeにおける注意点
開発モードでReactのStrict Modeが有効だと、副作用とクリーンアップが二回実行されることがあります。これは副作用が安全かを検出するための仕組みです。本番ビルドでは一回になりますが、開発時はクリーンアップが正しく動くかを確認しておくと安心です。
9. 実務的なベストプラクティス
- 副作用は目的ごとにuseEffectを分ける
- 登録したハンドラは関数参照として定義しておく(無名関数を避ける)
- 非同期はキャンセルやフラグで安全に処理する
- 依存配列を正しく指定して不要な再実行を避ける
10. デバッグのコツ
クリーンアップの挙動を確認するときは、useEffectの最初とクリーンアップ内でconsole.logを出して実行回数とタイミングを確認します。ネットワークやタイマーの挙動はブラウザの開発者ツールで追跡すると原因が分かりやすくなります。
11. チェックリスト
- 副作用を始めたら必ずクリーンアップを用意する
- イベントやタイマーは解除する
- 非同期はキャンセルまたはフラグで保護する
- Strict Modeでも安全に動くか確認する
まとめ
useEffectとクリーンアップ関数の重要ポイントの振り返り
ここまで、ReactのuseEffectにおけるクリーンアップ関数の役割や使い方について、イベントリスナー、タイマー、非同期処理といった具体例を通して学んできました。useEffectはコンポーネントの表示や更新に合わせて処理を実行できる非常に便利な仕組みですが、その分「開始した処理を正しく終了させる」意識が欠かせません。
クリーンアップ関数は、useEffectの中で副作用を開始した後、その副作用を安全に終了させるための重要な仕組みです。イベントリスナーの解除やタイマーの停止、通信処理の中断などを行うことで、不要な処理が残り続けることを防ぎ、Reactアプリケーション全体の安定性とパフォーマンスを保つことができます。特にコンポーネントのアンマウント時や、依存配列の値が変化したタイミングでクリーンアップが実行される点は、実務でも頻繁に意識するポイントです。
また、依存配列の指定によってクリーンアップの呼ばれ方が変わる点も重要です。空配列を指定すればマウント時とアンマウント時のみ処理が行われ、値を指定すればその値が変化するたびに「前の処理を片付けてから新しい処理を行う」流れになります。この仕組みを正しく理解することで、二重登録や古い状態を参照したままのバグを未然に防ぐことができます。
サンプルプログラムで再確認する考え方
最後に、これまでの内容を意識したシンプルなサンプルを見てみましょう。イベントの登録と解除を行う基本的なuseEffectの例です。
useEffect(() => {
const onResize = () => {
console.log("window resized");
};
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
};
}, []);
このコードでは、画面サイズが変わったときの処理を登録し、コンポーネントが不要になったタイミングで確実に解除しています。登録と解除をセットで書くことで、コードの意図が明確になり、後から読み返したときにも理解しやすくなります。useEffectを使う際は「始めた処理には必ず終わりがある」という考え方を習慣にすることが大切です。
生徒
「useEffectって、処理を書く場所というイメージだけでしたけど、クリーンアップまで含めて考えないといけないんですね。イベントやタイマーをそのままにしておくと問題が起きる理由がよく分かりました。」
先生
「その通りです。Reactではコンポーネントが何度も表示されたり消えたりしますから、副作用を安全に管理することがとても重要です。クリーンアップ関数を正しく書けるようになると、実務レベルのコードに一歩近づきますよ。」
生徒
「依存配列が変わったときに、先にクリーンアップが呼ばれる仕組みも理解できました。二重にイベントが登録されるのを防げるのは安心ですね。」
先生
「ええ、とても大切なポイントです。開発中にStrict Modeで動かして、クリーンアップが正しく動いているか確認する習慣をつけると、トラブルを減らせます。」
生徒
「これからはuseEffectを書くときに、『これは後でどうやって片付けるんだろう』と考えながら書いてみます。」
先生
「その意識がとても大事です。useEffectとクリーンアップ関数を正しく使えるようになれば、Reactの理解が一段深まります。ぜひ実際のアプリでも積極的に使ってみてください。」