🎰

onWheelが終わった直後の処理を書くときのTips

2021/08/02に公開

結論

onWheelが終了した直後の処理を書くにはsetTimeoutclearTimeoutを使えばいい感じに実装できる

下記Gifはちょっとわかりづらいかもしれないが、wheelしている間はdeltaX(横方向への移動した量)をconsoleに出力しつつ、wheelが終わったらwheelが終了したよをconsoleに出力している

onWheelが終わった直後にconsole.logしてるgif

はじめに

とあるプロダクトと同じような機能を実装するタスクがあり、その機能を実装するためには横にwheelした後に処理を入れる必要があったが、WheelEventにそれらしいプロパティはなく、wheelが終了した後にどのように処理を入れるかわからず詰まった。
とあるプロダクト上でChromeのDevToolsを開き、ビルドされた後のjsと睨めっこして、wheelが終了した後にどのように処理を入れてるかを解読したら結構参考になったのでまとめることにした。

記事のターゲット

実装したコード

コード全容
import { useEffect, useState, WheelEvent } from 'react'

export const Test = (): JSX.Element => {
  const [wheelEndTimeout, setWheelEndTimeout] = useState<
    NodeJS.Timeout | undefined
  >(undefined)
  const [isWheel, setIsWheel] = useState(false)

  useEffect(() => {
    const clear = () => {
      if (wheelEndTimeout) {
        clearTimeout(wheelEndTimeout)
      }
    }
    return clear()
  }, [])

  const handleWheel = (e: WheelEvent<HTMLDivElement>) => {
    console.log(e.deltaX)

    setIsWheel(true)

    if (wheelEndTimeout) {
      clearTimeout(wheelEndTimeout)
      setWheelEndTimeout(undefined)
    }

    if (isWheel) {
      const wheelEndTimeoutId = setTimeout(() => {
        setIsWheel(false)
        console.log('wheelが終了したよ')
        // NOTE: ここにwheel終了後の処理を書く
      }, 100)
      setWheelEndTimeout(wheelEndTimeoutId)
    }
  }

  return (
    <div
      className="flex items-center justify-center w-96 h-screen bg-primary"
      onWheel={handleWheel}
    >
      <p>ここをホイールしてみて</p>
    </div>
  )
}

wheelイベントが終了した後にどのように処理を入れるか

  1. wheel中かどうかをコンポーネントのstateに保持する
  2. wheel中にsetTimeoutが呼び出されている場合はclearTimeoutする
  3. wheel中の場合は100ms後にwheel中かどうかのstateをfalseにするsetTimeoutを呼ぶ(setTimeoutのなかにwheel終了後の処理をかく)

ざっくり言うとsetTimeoutの中にwheelイベント終了時の処理を定義し、wheelイベントが発火されるたびにsetTimeoutの時間を100ms秒伸ばしてます

const [wheelEndTimeout, setWheelEndTimeout] = useState<
    NodeJS.Timeout | undefined
  >(undefined) // setTimeoutを識別するためのidを保持
const [isWheel, setIsWheel] = useState(false) // wheel中かどうかの状態を保持

const handleWheel = (e: WheelEvent<HTMLDivElement>) => {
  console.log(e.deltaX)

  setIsWheel(true) // onWheel中のフラグを立てる

  if (wheelEndTimeout) {
    clearTimeout(wheelEndTimeout) // すでにsetTimeoutで100ms後にonWheel終了するようにしていたらsetTimeoutをclearする
    setWheelEndTimeout(undefined)
  }

  if (isWheel) {
    const wheelEndTimeoutId = setTimeout(() => {
      setIsWheel(false) // wheel中であれば100ms後にwheelが終了するフラグを立てる
      console.log('wheelが終了したよ')
      // NOTE: ここにwheel終了後の処理を書く

    }, 100)
    setWheelEndTimeout(wheelEndTimeoutId)
  }
}

念のため、アンマウント時にsetTimeoutが生きていた場合はclearするようにuseEffectで定義しておく

useEffect(() => {
  const clear = () => {
    if (wheelEndTimeout) {
      clearTimeout(wheelEndTimeout)
    }
  }
  return clear()
}, [])

所感

wheelイベントやscrollイベントなど、そのままのイベントプロパティでは終了した状態を把握するのが難しい場合に、setTimeoutとclearTimeoutを駆使して終了時の処理を書くのは手法の一つとして良さげな気がした

GitHubで編集を提案

Discussion