🧶

複数要素に対応したIntersection Observerのカスタムフック実装

2022/08/13に公開

はじめに

複数の要素に対してIntersection Observerで同じ挙動をさせるのにカスタムフックがあると便利だなと思ったので実装してみました。もちろん、単体の要素にも使えます。React + TypeScriptで実装してます。

Intersection Observer とは

ページをスクロールして要素がビューポート内に表示されたタイミングで要素をふわっと表示させるアニメーションをさせたい場合などに使います。

スクロールに合わせてアニメーション

https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API

交差オブザーバー API (Intersection Observer API) は、ターゲットとなる要素が、祖先要素または文書の最上位のビューポートと交差する変化を非同期的に監視する方法を提供します。

カスタムフックの実装

hooks/use-intersection-observer.ts
import { RefObject, useEffect } from 'react'

/**
 * @param refs Intersection Observer を適用した動作をさせたい要素の RefObject の配列
 * @param callback IntersectionObserver のインスタンス生成時に渡すコールバック関数
 * @param options IntersectionObserver のインスタンス生成時に渡すオプション
 */
export const useIntersectionObserver = (
  refs: RefObject<HTMLElement>[],
  callback: (entries: IntersectionObserverEntry[]) => void,
  options?: IntersectionObserverInit
): void => {
  useEffect(() => {
    const observer = new IntersectionObserver(callback, options)

    refs.forEach((ref) => {
      if (ref.current) {
        observer.observe(ref.current)
      }
    })

    return () => {
      refs.forEach((ref) => {
        if (ref.current) {
          observer.unobserve(ref.current)
        }
      })
    }
  })
}

実装解説

export const useIntersectionObserver = (
  refs: RefObject<HTMLElement>[],
  callback: (entries: IntersectionObserverEntry[]) => void,
  options?: IntersectionObserverInit
): void => { ...... }

第一引数refsには Intersection Observer を適用した動作をさせたい要素のRefObjectを配列で渡します。ここで渡した要素全てに対して同じ動作をさせることができます。

第二引数callbackと第三引数optionsは IntersectionObserver のインスタンス生成時に渡す引数です。つまり、第二引数には条件(要素がビューポート内に入ったなど)を満たした時に実行したい処理を関数で渡し、第三引数にはその条件を指定します。

export const useIntersectionObserver = (
  ......
): void => {
  useEffect(() => {
    const observer = new IntersectionObserver(callback, options)

    refs.forEach((ref) => {
      if (ref.current) {
        observer.observe(ref.current)
      }
    })

    return () => {
      refs.forEach((ref) => {
        if (ref.current) {
          observer.unobserve(ref.current)
        }
      })
    }
  })
}

関数の処理としては、受け取ったRefObjectの配列から要素を1つずつ取り出してobserveしてコンポーネントがアンマウントする時にunobserveするシンプルな実装になってます。

カスタムフックの使い方

任意の要素がビューポートに入ったタイミングでふわっと表示する実装は以下の通りです。

components/index.tsx
import { useIntersectionObserver } from '@/hooks/use-intersection-observer'
import { FC, useRef } from 'react'
import styles from './Component.module.scss'

// カスタムフックに渡すコールバック関数
const showElements = (entries: IntersectionObserverEntry[]) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      // IntersectionObserver で設定された条件を満たした時に実行する処理
      // 要素に active クラスを適用する
      entry.target.classList.add(styles.active)
    }
  })
}

const Component: FC = () => {
  const ref1 = useRef<HTMLHeadingElement>(null)
  const ref2 = useRef<HTMLHeadingElement>(null)
  const ref3 = useRef<HTMLHeadingElement>(null)

  // カスタムフックを呼ぶ
  useIntersectionObserver([ref1, ref2, ref3], showElements)

  return (
    <>
      <div>↓ scroll ↓</div>
      <h2 className={styles.heading} ref={ref1}>
        Hello 1
      </h2>
      <h2 className={styles.heading} ref={ref2}>
        Hello 2
      </h2>
      <h2 className={styles.heading} ref={ref3}>
        Hello 3
      </h2>
    </>
  )
}

export default Component
components/Component.module.scss
.heading {
  color: #0bd;
  font-size: 4rem;
  font-weight: bold;
  margin: 400px 0;
  opacity: 0;
  transform: translateX(-100px);
}

.active {
  opacity: 1;
  transition: 1s;
  transform: translateX(0);
}

実装の意図

Intersection Observer のインスタンスを生成する時に渡すコールバック関数とオプションをカスタムフックの引数にして外から注入できる仕様にすることで IntersectionObserver を使った動作や動作タイミングを外側からコントロール可能にしています。

参考文献

https://www.webcreatorbox.com/tech/intersection-observer

Discussion