🧹

Reactでクリーンアップ関数を書くべきタイミングとその理由

に公開

クリーンアップの書き方

クリーンアップなし(NG)

 useEffect(() => {
    setInterval(() => {
      setCount(prev => prev + 1);
    }, 1000); 

  }, []);

クリーンアップあり(OK)

"use client";
import React, { useEffect, useState } from "react";

const Page = () => {
  const [count, setCount] = useState(0);

 useEffect(() => {
    const timer = setInterval(() => {
      setCount(prev => prev + 1);
    }, 1000); 

    return () => {
      clearInterval(timer); // クリーンアップ:タイマー停止
    };
  }, []);


  return <div>{count}</div>;
};

export default page;

違いは、エフェクトの部分を変数に入れ、return文に接続解除の関数を書いているところです。

なぜクリーンアップが必要なのか

コンポーネントがアンマウントされても処理は裏で動き続けるからです。
まず前提として、それぞれのエフェクトは、コンポーネントのライフサイクルから独立させて考える必要があります。
以下はReact公式ドキュメントからの引用です。

すべての React コンポーネントは同じライフサイクルを持ちます。
・画面に追加されたとき、コンポーネントはマウントされます。
・(大抵はインタラクションに応じて)新しい props や state を受け取ったとき、コンポーネントは更新されます。
・画面から削除されたとき、コンポーネントはアンマウントされます。
これは、コンポーネントの考え方としては良いですが、エフェクトの考え方としては良くありません。それぞれのエフェクトは、コンポーネントのライフサイクルから独立させて考えましょう。エフェクトは、現在の props や state に外部システムをどのように同期させるのかを記述します。コードが変更されれば、同期の頻度も増減するでしょう。

多くの方は、アンマウントされたら同期も停止すると思いがちですが、実際はそうではありません。
コンポーネントがマウントされている間、同期の開始と停止を繰り返し行わなければならない場合があるからです。
つまり、エフェクトごとにきっちりクリーンアップを記述しないと、コンポーネントがアンマウントされても同期が停止されずにパフォーマンスを低下させる場合があります。

なお、クリーンアップの実行タイミングは、エフェクト実行の直前と、アンマウント時です。

クリーンアップが必要なケースは?

基本的に、「副作用が何かを登録・開始する場合」にはクリーンアップが必要です。
具体的には以下のような場合です。

ケース 説明 クリーンアップの方法
setInterval / setTimeout 処理が繰り返されたり遅延実行される。解除しないと動き続ける。 タイマーで数値をカウントアップ clearInterval() / clearTimeout()
addEventListener イベントリスナーが登録され続けてしまうと、重複実行・バグの原因になる。 windowresizemousemove を登録する場合 removeEventListener()
WebSocket, SSE (イベントストリーム) 常時サーバーと接続状態を保つ処理。破棄しないと通信が続いてしまう。 チャット・株価表示などのリアルタイム通信 socket.close() / eventSource.close()
fetch / Axios + AbortController 通信中にコンポーネントがアンマウントされると、エラーになる可能性がある。 APIリクエスト中に画面が切り替わる controller.abort()
外部ライブラリのインスタンス グラフ・地図などで初期化したインスタンスを破棄しないと、再表示で重複する。 Chart.js, Mapbox, Swiper, Video.js など destroy(), remove(), unmount() など
カスタムイベントやObserver IntersectionObserverなどを使った監視処理も放置するとリソースが無駄になる。 無限スクロール、画像遅延読み込みなど observer.disconnect()
サブスクリプション系(Redux/Firestoreなど) 状態の購読を解除しないと更新が続いてしまう。 FirestoreのonSnapshot, Reduxのsubscribe 返ってきた関数(unsubscribe() など)を実行

エフェクトは、多くの場合はクリーンアップ関数を返すべきですが、中には記述が必要ないケースもあります。
以下のようにエフェクトを一度きりしか呼ばない場合は必要ありません。
・APIを1回だけ呼ぶ(fetch など)
・ローカルストレージからデータを読む
・コンソールログを出すだけ
・一度だけ変数を変更する処理

Discussion