[React]スクロールに合わせてDOM要素を固定する。

親祖先要素にoverflowが使われている場合、position : stickyが使えない。その場合の対策として、HTMLのDOM構成を変える対策もあるが、本記事ではscrollイベントをリスナーしてstickyのような動作を実現させることを目的とした。

コード全体はこちら。

import { useEffect, useRef, useCallback, RefObject } from 'react'

export function StickyItem(boxRef: RefObject<HTMLDivElement>) {
  const stickyRef = useRef<HTMLDivElement>(null)

  const handleScroll = useCallback(() => {
    if (!stickyRef.current || !boxRef.current) return

    if (boxRef.current.getBoundingClientRect().top <= 50) {
      // fixed element
      stickyRef.current.style.position = 'absolute'
      stickyRef.current.style.top = Math.abs(boxRef.current.getBoundingClientRect().top) + 'px'
    } else {
      // change to static
      stickyRef.current.style.position = 'static'
    }
  }, [stickyRef, boxRef])

  useEffect(() => {
    window.addEventListener('scroll', handleScroll)

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

  return (
    <div style={{ position: 'relative' }}>
      <div
        style={{
          transition: 'top .7s ease',
          top: 0,
          willChange: 'top'
        }}
        ref={stickyRef}
      >
        <p>sticky contents</p>
      </div>
    </div>
  )
}

コンポーネントを呼び出したタイミングでscrollイベントの登録を行います。
StickyItemにスクロール位置を監視したい親要素をrefで受け取り、スクロール位置を監視しています。

渡されたboxRefのスクロール位置が50px以上になった時に、stickyRefのpositionをstaticからabsoluteに変更しています。
これでstickyのような動作を実現しています。

目次

Summary

ref要素のstyleを直接書き換えているが、stateで持った方が保守性が高くなると思います。(わかりやすい)
JavaScriptで実装していますが、scrollイベントは重いので、できる限りCSSで対応できるように変更した方が良さそうです。

よかったらシェアしてね!
目次