【React】そのUseEffect, 本当にcleanup(クリーンアップ)できてますか?

はじめに

こんにちは、株式会社TOKOSのツキヤです!
今回は、ReactのuseEffectを使う時に重要になるcleanup(クリーンアップ)について、簡単な説明とやってしまいそうなミスへの対策方法を書きます!
自分も1度ハマったので、誰かの参考になれば幸いです🙌

対象読者

  • Reactのcleanupについて知りたい方
  • 自分のcleanupが正しくできているか気になっている方

cleanup(クリーンアップ)とは?

まずはざっくりとcleanupについて説明します!

cleanupとは簡単に言うと「useEffectの中でreturnする関数」のことです!
例えば下記コードのような形です

import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // 副作用の処理(例:イベントリスナーの設定)
    const handleResize = () => {
      console.log('ウィンドウがリサイズされました!!');
    };
    window.addEventListener('resize', handleResize);

    // ここがクリーンアップ(関数)
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return <div>ウィンドウがリサイズされるとcosole.logが出力されるよ</div>;
}

上記のuseEffect内のreturnの部分です!
useEffect内で宣言したイベントリスナーをreturn内でremoveしています。
これにより、このMyComponentというコンポーネントがアンマウントされる時に一緒にイベントリスナーも消してくれるという感じです✨

逆に言うと、このcleanupが無い場合は、このコンポーネントがマウントされる度に同じイベントリスナーがどんどん追加されていき、重くなったり意図しない挙動になったりします🥲

ツキヤ
ツキヤ

今回の例で言うと、1回のリサイズでconsole.logが何回も出てしまう感じだね!

なので、基本的にはuseEffectを使う時はcleanupをする必要があるかは常に検討すべきです!

以下の記事が参考になります、詳しく知りたい方は見てみてください。

cleanup(クリーンアップ)ができていない実装

上記の例は、cleanupができている実装です!
上記のコードを再掲します。

import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // 副作用の処理(例:イベントリスナーの設定)
    const handleResize = () => {
      console.log('ウィンドウがリサイズされました!!');
    };
    window.addEventListener('resize', handleResize);

    // ここがクリーンアップ(関数)
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return <div>ウィンドウがリサイズされるとcosole.logが出力されるよ</div>;
}


それでは、cleanupができていない実装例はこちらです!

import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    window.addEventListener('resize', () => {
      console.log('ウィンドウがリサイズされました!!');
    });

    // ここがクリーンアップ(関数)
    return () => {
      window.removeEventListener('resize', () => {
        console.log('ウィンドウがリサイズされました!!');
      });
    };
  }, []);

  return <div>ウィンドウがリサイズされるとcosole.logが出力されるよ</div>;
}

handleResizeとして宣言していた部分をaddEventListenerremoveEventListenerの第二引数内に直接アロー関数として全く同じものをそれぞれ記述した形になります。

一見同じ挙動になりそうですね🤔

なぜできていないのか

これについては、関数は同じ記述をしたとしても、違うものとしてみなされるためです!
分かりやすい例をコードとして記載します!

const foo = 1;
const bar = 1;
console.log(foo === bar) // => true が出力される✅


const hoge = () => { console.log('ウィンドウがリサイズされました!!') };
const huga = () => { console.log('ウィンドウがリサイズされました!!') };
console.log(hoge === huga) // => false が出力される⛔️

上記のような結果になるのです🧐

1や”文字列” のようなプリミティブな値は変数に格納しても等価演算子(==)でその値同士が同じであればtrueになります。
一方、関数やオブジェクト(ex. { name: “ツキヤ” }) は全く同じ値を変数に格納しても、等価演算子で比較するとfalseになります!

これを説明するためには、JavaScriptのやや踏み込んだ理解が必要になるので割愛します🙏
詳しくはこちらの書籍等を参考にしてみてください。

useEffectの話に戻ると、addEventListenerremoveEventListenerのそれぞれで全く同じ関数を記述したとしても、先程の例から分かるように同じ関数としてみなされないです。
なので、全く違う関数をremoveEventListenerしようとしていることになり、うまくremoveEventListenerができないのです!!

そこで、初回の例のようにuseEffect内であらかじめ変数に関数を格納しておき、その変数をaddEventListenerremoveEventListenerのそれぞれの第二引数に渡すことで意図通りのcleanupとなります✨

終わりに

今回は、cleanupの簡単な説明と正しいcleanupの方法についての解説をしました!
Next.js等のフレームワークを上手くを使うためにも、React自体の機能をしっかり理解しておきたいですね😎