無限スクロール(IntersectionObserver)
:LiTarget: 用途
末尾の sentinel 要素が見えたら次ページ読込。IntersectionObserver で軽量。
:LiSparkle: 特徴
- IntersectionObserver
- ローディング状態
- 終端検知
- エラーハンドル
:LiCode: コード(コピペ用)
import { useEffect, useRef, useState } from 'react';
export function useInfiniteScroll<T>(fetcher: (page: number) => Promise<T[]>) {
const [items, setItems] = useState<T[]>([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [done, setDone] = useState(false);
const sentinelRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!sentinelRef.current) return;
const io = new IntersectionObserver(async (entries) => {
if (!entries[0].isIntersecting || loading || done) return;
setLoading(true);
try {
const next = await fetcher(page);
if (next.length === 0) setDone(true);
else { setItems(prev => [...prev, ...next]); setPage(p => p + 1); }
} finally { setLoading(false); }
}, { rootMargin: '200px' });
io.observe(sentinelRef.current);
return () => io.disconnect();
}, [page, loading, done, fetcher]);
return { items, loading, done, sentinelRef };
}
// 使い方:
// const { items, loading, sentinelRef } = useInfiniteScroll(p => api.fetch(p));
// {items.map(...)}
// <div ref={sentinelRef}>{loading ? 'Loading...' : null}</div>
:LiHandPointer: 使い方
対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。
:LiAlertCircle: 注意事項
- 依存パッケージを忘れず追加