useEffectでクリーンアップ関数を使う方法をやさしく解説
生徒
「先生、useEffectの中でやった処理を後で片付ける方法ってありますか?」
先生
「ありますよ。useEffectはクリーンアップ関数を返すことで登録したイベントやタイマーを解除したり、リソースを解放したりできます。」
生徒
「クリーンアップって具体的にどういうことをするんですか?」
先生
「例えばイベントリスナーの解除やタイマーの停止、非同期処理のキャンセルなどです。実例を見ながら説明しましょう。」
1. クリーンアップ関数とは?わかりやすい説明
クリーンアップ関数とは、useEffectの中で実行した処理を「必要なくなったときに片付けるための関数」のことです。たとえばイベントリスナーを登録したままにしておくと、コンポーネントが消えても動き続けてしまい、思わぬ不具合につながることがあります。そうした無駄な動作を防ぐために、useEffectは“後片付け”としてクリーンアップ関数を返せる仕組みになっています。
日常の例に置き換えると「電気をつけたら、部屋を出るときに必ず消す」「料理を始めたら、使い終わった調理器具は片付ける」といった考え方に近く、Reactでも“始めたら終わりを用意する”というイメージで覚えておくと理解しやすくなります。
初心者向けのかんたん例
ここでは非常にシンプルな例として、「コンポーネントが表示されたときにログを出し、消えるときに別のログを出す」というサンプルを紹介します。実際の開発でもデバッグ用途としてよく使うパターンです。
import React, { useEffect } from "react";
function SimpleCleanup() {
useEffect(() => {
console.log("表示されました!");
return () => {
console.log("消えました(クリーンアップ)");
};
}, []);
return (
<div>
<h2>クリーンアップの基本サンプル</h2>
<p>開発者ツールでログを確認してみましょう。</p>
</div>
);
}
export default SimpleCleanup;
このように、クリーンアップ関数は難しいものではなく、「useEffectの中で行った準備を終わらせる場所」と考えると理解しやすくなります。後の章で扱うタイマーや非同期処理でも、この仕組みが活躍します。
2. 基本的な書き方
useEffectは「何かを始める処理」と「終わらせる処理」をセットで書ける特別な関数です。実行したい副作用(イベント登録・データ取得・タイマー開始など)を書くと同時に、不要になったときの後片付けもまとめて書けるため、Reactではとても重要な役割を持っています。まずは基本の書き方を押さえておきましょう。
useEffect(() => {
// 副作用の処理(開始する処理)
return () => {
// クリーンアップ処理(終了するときの処理)
};
}, [依存配列]);
useEffectは、関数の中で実行した副作用を「いつクリーンアップするか」を依存配列によって制御できます。依存値が変わったときにはまずクリーンアップが実行され、そのあとに新しい副作用がもう一度実行されます。これにより古い処理が残ったまま動き続けるといったトラブルを避けられます。
初心者向けのかんたん例
ここでは、「画面を開いたときに挨拶を表示し、画面を離れたときに別のメッセージを残す」というシンプルな例で、基本形のイメージをつかんでみましょう。
import React, { useEffect } from "react";
function BasicUseEffect() {
useEffect(() => {
console.log("コンポーネントが表示されました");
return () => {
console.log("コンポーネントが消えました(クリーンアップ)");
};
}, []);
return (
<div>
<h2>useEffect の基本例</h2>
<p>ログを見ながら動作を確認してみましょう。</p>
</div>
);
}
export default BasicUseEffect;
このように、useEffectは「何かを始める」と「終わらせる」をひとまとめに管理できる便利な仕組みです。まずはこの基本形をしっかり理解しておくと、後の章で扱うイベントの登録・タイマー・非同期処理でも応用しやすくなります。
3. 例①:イベントリスナーの登録と解除
代表的な例はイベントリスナーです。コンポーネントが表示されたときにリスナーを登録し、消えるときに解除します。
useEffect(() => {
const handler = (e) => console.log(e.key);
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, []);
ポイントは、登録したのと同じ関数参照を渡して解除することです。無名関数で登録してしまうと解除が失敗します。
4. 例②:タイマーの開始と停止
setIntervalやsetTimeoutを使うときもクリーンアップで止めます。止め忘れるとタイマーが残り続けて無駄な処理が走ります。
useEffect(() => {
const id = setInterval(() => console.log("tick"), 1000);
return () => clearInterval(id);
}, []);
このようにタイマーIDを保持しておき、クリーンアップでclearIntervalを呼びます。
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でログを出しながら動きを追いかけてみることです。副作用が走ったタイミングと、クリーンアップが呼ばれたタイミングを自分の目で確認すると、Reactの挙動がぐっと理解しやすくなります。
例えば、次のようにuseEffectの中とクリーンアップの中にそれぞれログを仕込んでおくと、コンポーネントのマウントとアンマウントのタイミングがはっきり見えるようになります。
import React, { useEffect } from "react";
function DebugSample() {
useEffect(() => {
console.log("effect が実行されました");
return () => {
console.log("クリーンアップが実行されました");
};
}, []);
return (
<div>
<h2>デバッグ用コンポーネント</h2>
<p>コンソールを開いてログを確認してみましょう。</p>
</div>
);
}
export default DebugSample;
ブラウザの開発者ツールを開き、Consoleタブを見ながら画面の表示・非表示やコンポーネントの切り替えを行うと、どのタイミングでクリーンアップが呼ばれているかがよく分かります。タイマーやイベントリスナーを使っている場合も同じで、「登録するところ」と「解除するところ」の両方にログを仕込んでおくと、二重登録になっていないか、クリーンアップが漏れていないかを簡単に確認できます。慣れてきたら、ネットワークタブでfetchの回数を見たり、タイマーの動きを追ってみるのもおすすめです。まずは小さなコンポーネントで挙動を観察し、ReactのuseEffectとクリーンアップの流れを自分の手でつかんでいきましょう。
11. チェックリスト
useEffectを使うときに意識しておきたい大事なポイントを、初心者でもすぐ確認できる形で整理しました。特にクリーンアップは忘れやすく、予期しない動作につながることもあるため、チェックリストとして手元に置いておくと安心です。
- 副作用(イベント登録やタイマー開始など)を始めたら、必ず後片付けのクリーンアップを用意する
- イベントリスナーやタイマーは不要になったら確実に解除する
- 非同期処理はキャンセル処理またはフラグで安全に制御する
- Strict Modeでも同じように動くかを確認し、挙動を理解しておく
初心者向けかんたんサンプル
ここでは「ボタンを押すとメッセージが変わる」という、Reactの基本的な副作用とクリーンアップの考え方をイメージしやすいシンプルな例を紹介します。
import React, { useEffect, useState } from "react";
function SampleCheck() {
const [text, setText] = useState("こんにちは!");
useEffect(() => {
const timer = setTimeout(() => {
console.log("メッセージが更新されました");
}, 1000);
return () => clearTimeout(timer);
}, []);
return (
<div>
<h2>{text}</h2>
<button onClick={() => setText("ボタンがクリックされました!")}>
メッセージ変更
</button>
</div>
);
}
export default SampleCheck;
このサンプルのように、短いコードでも副作用とクリーンアップがどのように働いているかを確認できます。とくにタイマーの解除は忘れがちなので、どんな処理でも「始めたら必ず片付ける」という意識を持つと、Reactのコードはぐっと安定したものになります。
まとめ
useEffectのクリーンアップ関数を振り返ると、Reactアプリにおける副作用の扱いがどれほど重要であるかがよく分かります。画面の描画とは直接関係のない処理、例えばイベントリスナーの登録やタイマーの起動、非同期処理の進行などは、副作用としてコンポーネントが動くたびに実行されます。しかし、それらを放置したままコンポーネントが消えたり依存値が変わったりすると、予期しない動作やメモリリークにつながってしまいます。そこで登場するのが「クリーンアップ関数」です。useEffectから返されるこの関数は、まさに“後片付け”を担当する存在で、不要になった処理やリスナーを解除し、アプリ全体の健全性を保つ役割を担っています。
クリーンアップ関数の基本的な書き方を理解しておくことで、Reactコンポーネントのライフサイクルがどのように動いているかを自然に理解できるようになります。依存配列が変わったとき、Reactはまず古い副作用をクリーンアップしてから新しい副作用を実行します。この仕組みによって、古い登録や不必要な処理が重複することを防ぎ、安全で効率的な動作が保証されます。特に、イベントリスナーの例やタイマーの例では、登録した関数を正しく解除することがいかに大切かがよく分かります。無名関数を使ってしまうと解除が失敗し、気づかないうちにリスナーが増えてしまうケースは実際の現場でも多く見られます。クリーンアップは、Reactを扱ううえでの「基本でありながら最も重要なポイント」のひとつと言えるでしょう。
非同期処理のクリーンアップもまた大切な要素で、コンポーネントがすでにアンマウントされているのにsetStateを呼び出してしまうような問題を防ぐために、キャンセルフラグやAbortControllerを用いる方法が広く使われています。特にAbortControllerはブラウザ標準のAPIとして用意されており、fetch自体を安全に中断できるため、今後ますます使われる機会が増えるでしょう。このような最新の方法を取り入れることで、副作用の制御をより柔軟に、かつ確実に行えるようになります。
また、Strict Modeの項目で触れたように、開発モードではuseEffectが二度実行されることがあります。これはエラー検出のための仕様であり、本番では一度だけ実行されます。初心者が混乱しやすいポイントですが、これもクリーンアップ処理を正しく書いていれば問題になりません。クリーンアップはReactの副作用管理の中心であり、実務で使う頻度も非常に高いため、今回の内容をしっかり理解しておくことで、より安定したアプリケーション開発が可能になります。
クリーンアップを正しく扱うための実践サンプル
以下では、イベント・タイマー・非同期処理すべてを統合した応用例を紹介します。
import React, { useEffect, useState } from "react";
function CleanupExample() {
const [count, setCount] = useState(0);
const [data, setData] = useState(null);
useEffect(() => {
// キー入力イベント登録
const handler = (e) => console.log("押されたキー:", e.key);
window.addEventListener("keydown", handler);
// タイマー開始
const id = setInterval(() => setCount((c) => c + 1), 1000);
// fetchのキャンセル制御
const ac = new AbortController();
fetch("/api/info", { signal: ac.signal })
.then((r) => r.json())
.then((res) => setData(res))
.catch((err) => {
if (err.name !== "AbortError") console.error(err);
});
// クリーンアップで全て解除
return () => {
window.removeEventListener("keydown", handler);
clearInterval(id);
ac.abort();
};
}, []);
return (
<div className="card p-3" style={{ margin: "10px" }}>
<h2>クリーンアップ統合サンプル</h2>
<p>カウント: {count}</p>
<p>取得データ: {JSON.stringify(data)}</p>
</div>
);
}
export default CleanupExample;
このサンプルでは、イベント登録・タイマー・fetch処理をまとめて使っていますが、クリーンアップを適切に整えることで、複雑なコンポーネントでも安全かつ整然とした副作用処理を実現できます。Reactの実務では、このような複数の副作用が同時に発生するケースが珍しくありません。だからこそ、useEffectとクリーンアップの正しい扱いが重要になります。
生徒
「クリーンアップ関数って、ただ片付けるためのものだと思っていましたが、アプリの安全性を守るとても重要な仕組みなんですね!」
先生
「その通りです。特にイベントリスナーやタイマーは残り続けるとバグの原因になるので、必ず解除する必要があります。」
生徒
「非同期処理のキャンセルも大事なんですね。AbortControllerって便利だと感じました!」
先生
「モダンなReactではよく使われますから、ぜひ覚えておきましょう。fetchの中断ができると安全なコードになりますよ。」
生徒
「今回のサンプルで副作用が全部整理されていて、とても理解しやすかったです!いろいろ組み合わせてもクリーンアップを正しく書けば大丈夫なんですね。」
先生
「ええ、Reactの副作用は扱い方さえ守ればとても強力です。これからも実際に書きながら覚えていきましょう。」