🧘

無限スクロール/ローディングなUIを作るときの汎用hooks

2022/07/07に公開

無限スクロール/ローディングなUIを作る機会が多いので、共通化できる部分をhooksにして再利用できるようにしました。
IntersectionObserverを使ってリストの最後の要素の位置を判定して次ページのコンテンツを読み込むなどの処理を実現したいときに使えます。

import { useEffect, useRef, useState } from 'react'

export const useInfiniteScroll = (
  loadNextPage: () => void,
  options?: IntersectionObserverInit
) => {
  const [lastElement, setLastElement] = useState<HTMLElement | null>(null)
  const observer = useRef<IntersectionObserver | null>(null)

  useEffect(() => {
    observer.current = new IntersectionObserver(
      (entries) => {
        const first = entries[0]
        if (first.isIntersecting) {
          loadNextPage()
        }
      },
      {
        root: options?.root ? options.root : undefined,
        rootMargin: options?.rootMargin ? options.rootMargin : '10px',
        threshold: options?.threshold ? options.threshold : 0,
      }
    )
  }, [loadNextPage, options])

  useEffect(() => {
    if (observer.current === null) return

    const currentElement = lastElement
    const currentOvserver = observer.current
    if (currentElement) {
      currentOvserver.observe(currentElement)
    }

    return () => {
      currentOvserver.disconnect()
    }
  }, [lastElement])

  return {
    lastElement,
    setLastElement,
  }
}

使い方

swrやReact QueryのuseInfiniteQueryなどと合わせて使うとシュッと書けます。

ArticleList.tsx
const handleReachEnd = useCallback(() => {
  // 次ページのコンテンツを読み込む処理等
  fetchNextPage()
}, [fetchNextPage])
  
const { setLastElement } = useInfiniteScroll(handleReachEnd, {
  // IntersectionObserverのオプション
  root: scrollRootElementRef.current ?? undefined,
  rootMargin: '100px 0px 0px 0px',
})

return (
  <div>
    {articles.map((article, i) => (
      <ArticleCard
        key={i}
        setRef={
          i === articles.length - 1
            ? (ref) => {
                setLastElement(ref)
              }
            : undefined
        }
	article={article}
      />
    ))}
  </div>
)
ArticleCard.tsx
type Props = {
  setRef?: (ref: HTMLDivElement | null) => void
  article: Article
}
const ArticleCard: React.VFC<Props> = ({ setRef, article }) => {
  return <div ref={setRef}>{article.title}</div>
}

Discussion