示例说明
无限列表的核心是把分页状态交给查询库托管:useInfiniteQuery 维护页面数组与下一页游标,fetchNextPage 追加页面而不是替换,flatMap 展平全部条目渲染。staleTime 让已加载页面保持稳定,keepPreviousData 保证翻页或刷新瞬间旧内容不消失,按钮的禁用态由 hasNextPage 与取数状态共同决定。
技术要点
- pageParam 把游标传给取数函数
- getNextPageParam 决定终止与续取
- prefetchInfiniteQuery 提前预热下一页
代码解读
fetchFeed 组装带 limit 与 cursor 的查询参数;queryFn 从 pageParam 接收上一页的 nextCursor,首屏为 undefined。getNextPageParam 返回 undefined 即 hasNextPage 为假。结尾的预取调用在用户滚动前就把下一页取到缓存,点击加载时零等待。
使用建议
缓存键要包含影响结果的筛选条件。窗口化渲染长列表配合虚拟滚动组件。预取只在视口接近底部时触发,避免无谓流量。
代码示例
import {
useInfiniteQuery, queryClient, keepPreviousData,
} from '@tanstack/react-query';
interface FeedPage {
items: { id: string; title: string }[];
nextCursor: string | null;
}
async function fetchFeed(cursor?: string): Promise<FeedPage> {
const params = new URLSearchParams({ limit: '20' });
if (cursor) params.set('cursor', cursor);
const res = await fetch('/api/feed?' + params.toString());
if (!res.ok) throw new Error('feed failed: ' + res.status);
return res.json();
}
function useFeed() {
return useInfiniteQuery({
queryKey: ['feed'],
queryFn: ({ pageParam }) => fetchFeed(pageParam),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
staleTime: 30_000,
placeholderData: keepPreviousData,
});
}
function FeedList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useFeed();
const items = data?.pages.flatMap((page) => page.items) ?? [];
return (
<section>
{items.map((item) => <article key={item.id}>{item.title}</article>)}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'loading…' : 'load more'}
</button>
</section>
);
}
queryClient.prefetchInfiniteQuery({
queryKey: ['feed'],
queryFn: ({ pageParam }) => fetchFeed(pageParam),
initialPageParam: undefined as string | undefined,
pages: 1,
});