概要
useInfiniteQuery を使うと、ページネーション API の結果を累積しながら取得できる。
Intersection Observer と組み合わせることで、リストの末尾が表示されたタイミングで自動的に次ページをフェッチする無限スクロールを実現できる。
インストール
npm install @tanstack/react-query
実装例
// src/components/InfinitePostList.tsx
"use client";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useEffect, useRef } from "react";
type Post = { id: number; title: string; body: string };
type PageData = {
posts: Post[];
nextPage: number | undefined;
};
async function fetchPosts({ pageParam = 1 }: { pageParam: number }): Promise<PageData> {
const limit = 10;
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts?_page=${pageParam}&_limit=${limit}`
);
const posts: Post[] = await res.json();
return {
posts,
nextPage: posts.length === limit ? pageParam + 1 : undefined,
};
}
export function InfinitePostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useInfiniteQuery({
queryKey: ["posts", "infinite"],
queryFn: fetchPosts,
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.nextPage,
});
// リスト末尾の要素を監視する ref
const loadMoreRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = loadMoreRef.current;
if (!el) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
},
{ threshold: 0.1 }
);
observer.observe(el);
return () => observer.disconnect(); // cleanup
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
if (isLoading) {
return <p className="text-sm text-gray-400">読み込み中...</p>;
}
const allPosts = data?.pages.flatMap((page) => page.posts) ?? [];
return (
<div className="space-y-3">
{allPosts.map((post) => (
<div key={post.id} className="rounded border px-4 py-3 text-sm">
<p className="font-medium">{post.title}</p>
<p className="text-gray-500">{post.body}</p>
</div>
))}
{/* この要素が画面内に入ると次ページをフェッチする */}
<div ref={loadMoreRef} className="py-2 text-center text-sm text-gray-400">
{isFetchingNextPage
? "読み込み中..."
: hasNextPage
? "スクロールして続きを読む"
: "これ以上ありません"}
</div>
</div>
);
}
ポイント
getNextPageParamがundefinedを返すとhasNextPageがfalseになりfetchNextPageが無効化されるdata.pages.flatMap(page => page.posts)で全ページの結果を1つの配列にフラット化するIntersectionObserverはuseEffectの return でdisconnect()して必ずクリーンアップするisFetchingNextPageで追加フェッチ中の状態を管理し、二重リクエストを防ぐinitialPageParamは TanStack Query v5 から必須(v4 ではdefaultPageParamはなく初期値がundefined)