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?

Reactでイベントリスナーと状態更新を扱うときの落とし穴

Last updated at Posted at 2025-06-24

はじめに

ReactでaddEventListenerを使うとき、こんなふうに書いたことはありませんか?

const handleScroll = () => {
  console.log("scroll")
}

useEffect(() => {
  window.addEventListener("scroll", handleScroll)
  return () => {
    window.removeEventListener("scroll", handleScroll)
  }
}, [])

一見これで十分に思えますが、実はこの書き方、Reactでは危険な罠を含んでいます。

本記事では、Reactでイベントリスナーを安全に扱うためのコツを、2つの代表的な罠とその実践的な解決法を通じて整理します。

1. イベントリスナーの関数参照問題

❌ 何が問題?

JavaScriptでは、removeEventListeneraddEventListener に渡したのと 「同じ関数参照」 でなければリスナーを削除できません。

しかし、Reactでは関数コンポーネントが再レンダリングされるたびに handleScroll のような関数は毎回新しく生成されるため、参照が一致しない可能性があります。

const handleScroll = () => {
  console.log("scroll")
}

useEffect(() => {
  window.addEventListener("scroll", handleScroll)
  return () => {
    window.removeEventListener("scroll", handleScroll) // ❌ 別参照になっているかも
  }
}, [])

✅ 解決法1:useCallbackで関数をメモ化

const handleScroll = useCallback(() => {
  console.log("scroll")
}, [])

useEffect(() => {
  window.addEventListener("scroll", handleScroll)
  return () => {
    window.removeEventListener("scroll", handleScroll)
  }
}, [handleScroll])

useCallback を使うことで、同じ関数参照が保たれ、登録と解除が一致します。

✅ 解決法2:useEffectの中で定義する

useEffect(() => {
  const handleScroll = () => {
    console.log("scroll")
  }

  window.addEventListener("scroll", handleScroll)
  return () => {
    window.removeEventListener("scroll", handleScroll)
  }
}, [])

同じスコープで定義されることで、addremoveの関数参照が常に一致する安全な書き方です。

2. stateの最新値が保証されない

たとえば、タブの切り替え検出イベントでカウントを増やす場合:

useEffect(() => {
  const handleChange = () => {
    if (document.visibilityState !== "visible") {
      setCount(count + 1) // ❌ 最新のcountかはわからない
    }
  }

  document.addEventListener("visibilitychange", handleChange)
  return () => {
    document.removeEventListener("visibilitychange", handleChange)
  }
}, [count]) // ❗ 依存にcountを入れるとEffectが再実行される

❌ 問題1:クロージャで古い count を参照してしまう

count はクロージャに閉じ込められており、イベント発火時にはすでに古い値になっている可能性があります。
その結果、連続でイベントが発生しても1回しかカウントされないなどのバグが起こり得ます。

❌ 問題2:依存配列に count を入れると、Effectが毎回再実行される

useEffectの依存配列に count を含めると、count が更新されるたびに

  • イベントリスナーが毎回登録・解除され
  • 結果として不要な再登録が繰り返される

という非効率な処理になります。

✅ 解決法:useCallbackまたはuseEffect内でhandlerを定義

依存にstateを入れたくない場合は、handler内でstateを使わない書き方に変えるのがコツです。

useEffect(() => {
  const handleChange = () => {
    if (document.visibilityState !== "visible") {
      setCount((prev) => prev + 1)
    }
  }

  document.addEventListener("visibilitychange", handleChange)
  return () => {
    document.removeEventListener("visibilitychange", handleChange)
  }
}, []) // ✅ countを依存に入れなくてOK

まとめ

Reactでイベントリスナーを扱うときは、次の2つの罠に特に注意してください。

🕳 罠1:リスナー関数の参照が毎回変わる

  • 🔧 解決法1:useCallbackでメモ化
  • 🔧 解決法2:useEffect内で関数を定義

🕳 罠2:stateの最新値が取れない

  • 🔧 解決法:handler内でstateを直接使わない構成にする
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?