636 字
3 分钟
触底分页加载模版代码
基本状态
基本的状态可以分成 5 个:
const [data, setData] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
data
储存列表数据,page
是当前页码,hasMore
表示已经结束,isLoading
是加载中的状态,得和 isRefreshing
刷新中区分开来,原因后面会讲。
触底条件
尺寸计算
触底判断可以分成两种,兼容性更好的方案是通过尺寸属性去计算:
scrollHeight - (scrollTop + clientHeight) < threshold
三个属性的意义如下:
scrollHeight
是容器的总高度,包括不可见的滚动区域scrollTop
是已经滚动完成的高度,也就是滚出去的上半部分高度clientHeight
容器自身高度(包括 padding)
所以计算下来 scrollHeight - (scrollTop + clientHeight)
指的是容器还没有滚动到的剩余高度(下方不可见部分),当他小于阈值 threshold
时触发加载。
observer
性能更好的方案,不会频繁触发布局:
const observer = useRef();
const sentinelRef = useCallback(node => {
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasMore && !isLoading && !isRefreshing) {
loadMoreData();
}
}, { rootMargin: '100px' });
node && observer.current.observe(node);
}, [hasMore, isLoading, isRefreshing]);
同时在列表最下方加入探针元素:
<div ref={sentinelRef} style={{ height: 1 }} />
在探针滚动到可见的时候,触发 observer
,判断当前终止条件和加载中,避免多次触发。
加载更多
const loadData = async (targetPage = page, isRefresh = false) => {
try {
isRefresh ? setIsRefreshing(true) : setIsLoading(true);
const newData = await fetchData(targetPage);
setData(prev =>
targetPage === 1 ? newData : [...prev, ...newData]
);
setHasMore(newData.length >= 10); // 假设每页10条
setPage(targetPage + 1);
} finally {
isRefresh ? setIsRefreshing(false) : setIsLoading(false);
}
};
加载后的列表数据拼接到 data
之后,setHasMore
判断是否终止,setPage
设置新页码。
刷新
const handleRefresh = () => {
if (isRefreshing) return;
setPage(1); // 重置页码
setHasMore(true); // 重置数据终止标记
loadData(1, true); // 强制从第一页加载
};
前面提到的 isLoading
和 isRefreshing
就是为了区分 loadMore
和 refresh
。
为什么不复用 isLoading,原因有几个:
- 要区分数据的赋值逻辑,
loadMore
是拼接,refresh
是重设 - 单状态管理会导致竞态条件,刷新和加载都共同维护
isLoading
,会增加复杂度,很可能出现双方通过异步先后修改isLoading
的情况 - 能显示不同的提示语
初始化
useEffect(() => {
loadData();
}, []);
额外功能
请求取消
const abortController = useRef(new AbortController());
useEffect(() => {
return () => abortController.current.abort();
}, []);
刷新防抖
const lastRefresh = useRef(0);
const handleRefresh = () => {
if (Date.now() - lastRefresh.current < 3000) return;
lastRefresh.current = Date.now();
// ...刷新逻辑
};