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 コンポーネントの動作をより深く理解できるようになるでしょう。