在 React 中使用 Intersection Observer API 实现触底加载分页(无限滚动)
1.基本实现思路
- 在列表底部放置一个 哨兵元素(Sentinel)(如
<div>)。 - 使用
IntersectionObserver监听该元素是否进入视口(即触底)。 - 触底时加载下一页数据,并更新列表。
2.完整代码实例
import { useState, useEffect, useRef } from 'react';
function InfiniteScrollList() {
const [data, setData] = useState([]); // 当前数据
const [page, setPage] = useState(1); // 当前页码
const [loading, setLoading] = useState(false); // 加载状态
const [hasMore, setHasMore] = useState(true); // 是否还有更多数据
const sentinelRef = useRef(null); // 哨兵元素的 ref
// 模拟异步加载数据
const fetchData = async () => {
if (loading || !hasMore) return;
setLoading(true);
// 模拟 API 请求(替换为实际接口)
const mockData = Array.from({ length: 10 }, (_, i) =>
`Item ${(page - 1) * 10 + i + 1}`
);
// 模拟延迟
await new Promise(resolve => setTimeout(resolve, 1000));
setData(prev => [...prev, ...mockData]);
setPage(prev => prev + 1);
setLoading(false);
// 假设第 5 页后无数据
if (page >= 5) setHasMore(false);
};
// 初始化 IntersectionObserver
useEffect(() => {
if (!sentinelRef.current || !hasMore) return;
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
fetchData(); // 触底时加载数据
}
},
{ threshold: 1.0 } // 当哨兵元素完全进入视口时触发
);
observer.observe(sentinelRef.current);
return () => {
if (sentinelRef.current) observer.unobserve(sentinelRef.current);
};
}, [page, hasMore, loading]); // 依赖项
return (
<div style={{ maxHeight: '400px', overflow: 'auto' }}>
<h2>无限滚动列表</h2>
<ul>
{data.map((item, index) => (
<li key={index} style={{ padding: '10px', borderBottom: '1px solid #eee' }}>
{item}
</li>
))}
</ul>
{/* 哨兵元素:用于检测触底 */}
<div ref={sentinelRef} style={{ height: '20px' }}>
{loading && <p>加载中...</p>}
{!hasMore && <p>没有更多数据了</p>}
</div>
</div>
);
}
export default InfiniteScrollList;
3.关键点说明
-
哨兵元素(Sentinel)
- 一个隐藏的
<div>作为触底标记,通过ref绑定到IntersectionObserver。
- 一个隐藏的
-
IntersectionObserver 配置
threshold: 1.0:当哨兵元素100%进入视口时触发回调。- 在
useEffect中初始化并清理观察器,避免内存泄漏。
-
加载控制逻辑
loading防止重复请求。hasMore标记数据是否全部加载完毕。
-
性能优化
- 使用
useCallback包裹 fetchData(如果函数逻辑复杂)。 - 实际项目中,结合分页接口的
total字段判断是否还有数据。
- 使用
4.实际项目适配
- 替换 fetchData 中的模拟请求为真实 API 调用(如 axios 或 fetch)。
- 可加入防抖(Debounce)优化频繁触发问题(如快速滚动时)。
简易封装触底自动加载数据组件 InfiniteScroll.jsx
import { useEffect, useRef } from 'react'
const InfiniteScroll = (props) => {
// children 可以设置触底时显示的提示文字,加载状态等
// loadMore 加载数据的接口方法
// hasMore 检查数据是否全部加载完毕,分页加载之后还有下一页数据保持 true 不变,确保后续不会再有数据则为 false
// rootElement 浏览器可视窗口或者自定义滚动视口
const { children, loadMore, hasMore, rootElement = null } = props
const sentinelRef = useRef(null)
useEffect(() => {
if (!sentinelRef.current || !hasMore) return;
const observer = new window.IntersectionObserver(
([{isIntersecting}]) => {
if (isIntersecting) {
// 加载数据接口调用,当该组件进入浏览器视口时就会触发
loadMore()
}
},
{ threshold: 1.0, root: rootElement }
);
observer.observe(sentinelRef.current);
return () => {
if (sentinelRef.current) observer.unobserve(sentinelRef.current);
};
}, [hasMore])
return (
// 哨兵容器,目标元素,用来监听是否加载数据,目标元素与视口交叉 isIntersecting 值就会变化
<div ref={sentinelRef} style={{ minHeight: '2px', textAlign: 'center' }}>
{children}
</div>
)
}
export default InfiniteScroll
使用示例
const CustomComponent = () => {
// 加载数据函数
const loadMore = () => {
// ...
}
return (
<>
// ...
// 循环数据,将组件放置在循环数据的底部即可
<InfiniteScroll loadMore={loadMore} hasMore={hasMore}>
{hasMore && <Spin indicator={<LoadingOutlined spin />} size="large" />}
</InfiniteScroll>
// ...
</>
)
}
1375

被折叠的 条评论
为什么被折叠?



