跳到主要内容

无限滚动与分页加载

场景

需要实现一个信息流列表,滚动到底部自动加载更多数据。

实现方案

IntersectionObserver 方案(推荐)

useInfiniteScroll Hook
import { useRef, useEffect, useCallback, useState } from 'react';

function useInfiniteScroll<T>(
fetchFn: (page: number) => Promise<{ data: T[]; hasMore: boolean }>
) {
const [items, setItems] = useState<T[]>([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const sentinelRef = useRef<HTMLDivElement>(null);

const loadMore = useCallback(async () => {
if (isLoading || !hasMore) return;
setIsLoading(true);
try {
const { data, hasMore: more } = await fetchFn(page);
setItems((prev) => [...prev, ...data]);
setHasMore(more);
setPage((p) => p + 1);
} finally {
setIsLoading(false);
}
}, [page, isLoading, hasMore, fetchFn]);

useEffect(() => {
const sentinel = sentinelRef.current;
if (!sentinel) return;

const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) loadMore();
},
{ rootMargin: '200px' } // 提前 200px 触发
);

observer.observe(sentinel);
return () => observer.disconnect();
}, [loadMore]);

return { items, isLoading, hasMore, sentinelRef };
}

// 使用
function FeedList() {
const { items, isLoading, hasMore, sentinelRef } = useInfiniteScroll(fetchPosts);

return (
<div>
{items.map((item) => <PostCard key={item.id} data={item} />)}
{isLoading && <Spinner />}
{/* 哨兵元素:进入视口时触发加载 */}
<div ref={sentinelRef} />
{!hasMore && <p>已经到底了</p>}
</div>
);
}

结合虚拟列表

当数据量超过数千条时,需要虚拟滚动避免 DOM 过多:

react-window + 无限滚动
import { FixedSizeList } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';

function VirtualInfiniteList({ items, hasMore, loadMore }: Props) {
return (
<InfiniteLoader
isItemLoaded={(index) => index < items.length}
itemCount={hasMore ? items.length + 1 : items.length}
loadMoreItems={loadMore}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
ref={ref}
height={600}
itemCount={items.length}
itemSize={80}
onItemsRendered={onItemsRendered}
>
{({ index, style }) => (
<div style={style}>
<PostCard data={items[index]} />
</div>
)}
</FixedSizeList>
)}
</InfiniteLoader>
);
}

常见面试问题

Q1: 无限滚动相比传统分页有什么优缺点?

答案

维度无限滚动传统分页
用户体验流畅,无中断需要点击翻页
SEO差(单页内容)好(每页独立 URL)
性能需虚拟列表优化每页 DOM 固定
定位难以分享特定位置URL 包含页码
适用信息流、社交媒体搜索结果、表格

Q2: 为什么用 IntersectionObserver 而不是 scroll 事件?

答案

IntersectionObserver 在浏览器合成线程中执行,不阻塞主线程,性能比 scroll 事件好很多。而且不需要手动节流,API 简洁。

相关链接