🧘
無限スクロール/ローディングなUIを作るときの汎用hooks
無限スクロール/ローディングな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