🌊

React+IntersectionObserverでスクロールエフェクトを実装

2023/10/16に公開

スクロールしたらフェードインする、よくあるスクロールエフェクトをReact環境で実装してみました。
基本はVanilla-JSでやっていたような動きをReactで実現するような実装となっています。

CSSライブラリはemotionを使っています。

フェードイン前、フェードイン後の共通CSSを設定

今回は下から上にフェードする動きをつけるので、共通のCSSとして定義しておきます。

export const beforeFadeIn = css`
  opacity: 0;
  transform: translateY(20px);
  transition: transform 0.5s ease-in-out, opacity 0.5s ease-in-out;
`
export const afterFadeIn = css`
  opacity: 1;
  transform: translateY(0);
`

フェードインの処理をカスタムフックとして設定

各コンポーネントで処理を共通化するためにカスタムフックとして切り出します。
IntersectionObserverを設定し、イベントが発火した際にisVisibleがtrueになるようにする。
一度だけ発火させたいため、isIntersectingがtrueの場合のみisVisibleをtrueにする。

引数としてthresholdrootMarginを設定できるようにしています。
指定がなければデフォルト値を適用するようにしています。

// useFadeIn.ts
import type { RefObject } from 'react'
import { useEffect, useRef, useState } from 'react'

interface UseFadeInOptions {
  threshold?: number
  rootMargin?: string
}

export const useFadeIn = <T extends HTMLElement>(
  options?: UseFadeInOptions
): { isVisible: boolean; targetRef: RefObject<T> } => {
  const [isVisible, setIsVisible] = useState(false)
  const targetRef = useRef<T>(null)

  useEffect(() => {
    const currentRef = targetRef.current
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry && entry.isIntersecting) {
          setIsVisible(true)
          observer.unobserve(entry.target)
        }
      },
      {
        root: null,
        rootMargin: options?.rootMargin || '-20% 0px -20% 0px',
        threshold: options?.threshold || 0,
      }
    )

    if (currentRef) {
      observer.observe(currentRef)
    }

    return () => {
      if (currentRef) {
        observer.unobserve(currentRef)
      }
    }
  }, [options, isVisible])

  return { isVisible, targetRef }
}

各コンポーネントに設定

各コンポーネントにカスタムフックと共通のCSSを読み込んで、フェードインさせる要素に指定します。
css部分はemotionを使ってフェード前にbeforeFadeInを指定し、フェード後はafterFadeInを指定しています。

import { useFadeIn } from '@/hooks/useFadeIn'
import { afterFadeIn, beforeFadeIn } from '@/styles/function'


export const Sample = (props: SampleProps) => {
  const { isVisible, targetRef } = useFadeIn<HTMLParagraphElement | HTMLDivElement | HTMLHeadingElement>()
  return (
    <div css={styles.container}>
      <div css={styles.titleWrap}>
        <p css={[styles.subTitle, beforeFadeIn, isVisible && afterFadeIn]} ref={targetRef}>
          {props.subTitle}
        </p>
        <h2 css={[styles.title, beforeFadeIn, isVisible && afterFadeIn]} ref={targetRef}>
          {props.title}
        </h2>
      </div>
    </div>
  )
}

スクロールに応じてフェードする機能は、多くのコンポーネントで共通の設定が求められることが多いです。そのため、カスタムフックとCSSを共通化することで、管理が効率的に行えるようになりました。

TAM

Discussion