0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

useEffectでaddEventListenerしたのに、クリック処理が何度も発火する話

0
Posted at

useEffectでaddEventListenerしたのに、クリック処理が何度も発火する話

Reactでウィンドウのリサイズやスクロールを監視する処理をuseEffectで書いたとき、「同じクリックで処理が2回、3回と実行されるようになった」という症状に出会うことがあります。今回はこの原因と直し方を整理します。

Before: クリーンアップを忘れたコード

function useOutsideClick(onOutsideClick: () => void) {
  useEffect(() => {
    function handleClick(e: MouseEvent) {
      onOutsideClick();
    }
    window.addEventListener("click", handleClick);
  }, [onOutsideClick]);
}

「要素の外側をクリックしたらモーダルを閉じる」のような処理でよく書かれるパターンです。一見問題なさそうですが、これを使うコンポーネントが再マウントされるたびに、addEventListenerだけが積み重なっていきます。

なぜ発火回数が増えていくのか

useEffectの中でaddEventListenerを呼ぶと、そのたびに新しいリスナーが登録されます。コンポーネントがアンマウントされる時に対応するremoveEventListenerを呼んでいないと、古いリスナーは削除されずに残ったままになります。

たとえばモーダルを開閉するたびにこのフックが動くコンポーネントがマウント・アンマウントを繰り返すと、リスナーの数はどんどん増えていきます。1回のクリックで、残っている全部のリスナーが反応するので、「クリックのたびに実行回数が増えていく」という現象になります。

エラーは出ないので、開発中に気づきにくいのも厄介な点です。

なお、コンポーネントがアンマウントされていなくても、依存配列に渡している値(この例ではonOutsideClick)が親から毎回新しい関数として渡されている場合、依存配列が変わったと判定されてuseEffectが再実行されます。この時もクリーンアップがなければ、古いリスナーが残ったまま新しいリスナーが追加されるので、同じ現象が起きます。

After: クリーンアップ関数で解除する

function useOutsideClick(onOutsideClick: () => void) {
  useEffect(() => {
    function handleClick(e: MouseEvent) {
      onOutsideClick();
    }
    window.addEventListener("click", handleClick);

    return () => {
      window.removeEventListener("click", handleClick);
    };
  }, [onOutsideClick]);
}

useEffectの中で返す関数(クリーンアップ関数)は、次にこのuseEffectが実行される直前と、コンポーネントがアンマウントされる時に呼ばれます。ここでremoveEventListenerを呼んでおくことで、リスナーが積み重なるのを防げます。

addEventListenerを書いたら、その場でセットでremoveEventListenerも書いてしまうくらいの感覚でいると、この手の見落としを減らせます。

見落としに気づくためのチェック方法

ブラウザの開発者ツールを使うと、実際にリスナーが増えているかを確認できます。

Elements タブ → html要素を選択 → Event Listeners タブ

windowに登録したリスナーは、html要素を選んだ状態で確認できます(「Ancestors」のチェックが入っているのが初期設定です)。同じイベント(clickなど)に対して、複数のハンドラが登録されていたら、クリーンアップ漏れのサインです。

AIへの確認の仕方

「クリック処理が複数回実行される」という症状だけでは、AIも原因を絞り込みにくいことがあります。次のように、関連するコードとセットで伝えるとスムーズです。

ボタンをクリックすると、処理が複数回実行されてしまいます。
useEffectでaddEventListenerしている箇所のコードを貼ります。
クリーンアップ関数でremoveEventListenerを
呼び忘れていないか確認してください。

(ここにuseEffect部分のコードを貼る)

「何度も実行される」という症状から、クリーンアップ漏れを連想するのは初見だと難しいので、原因の候補を伝えた上で確認してもらうのが近道です。


未経験者向けの講座を運営しています

未経験から Next.js + Supabase + Claude Code で Webアプリを公開するまで を、全24セッションで体系化した教材です。Claude Code を学習パートナーにする CLAUDE.md と学習モード(learner / developer)の設計までセットで含みます。

※ Qiita 読者の方には易しすぎる内容なので、初心者の知り合いへの紹介や社内研修の参考としてどうぞ。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?