一个功能强大、灵活的 React 无限滚动 Hook,支持页码分页、自定义加载条件和外部状态控制。
特性
- 🚀 页码分页:使用标准的页码参数,从第1页开始
- 🎯 灵活控制:支持外部函数判断是否还有更多数据
- 🔄 状态管理:内置加载状态、错误处理和数据重置
- 📱 性能优化:使用 refs 避免闭包问题,支持被动滚动监听
- 🛠 依赖控制:支持依赖条件,只有在满足条件时才初始化加载
- 🎨 TypeScript:完整的 TypeScript 支持
安装
bash1# 该 Hook 位于项目内部 2import { useInfiniteScroll } from '@/pages/data-sync/hooks';
基础用法
tsx1import React from 'react'; 2import { useInfiniteScroll } from '@/pages/data-sync/hooks'; 3 4interface DataItem { 5 id: string; 6 name: string; 7} 8 9const MyComponent = () => { 10 // 定义数据获取函数 11 const fetchData = async (page: number): Promise<DataItem[]> => { 12 const response = await fetch(`/api/data?page=${page}&size=20`); 13 const result = await response.json(); 14 return result.data; 15 }; 16 17 const { 18 data, 19 loading, 20 error, 21 containerRef 22 } = useInfiniteScroll(fetchData); 23 24 return ( 25 <div 26 ref={containerRef} 27 style={{ height: '400px', overflowY: 'auto' }} 28 > 29 {data.map(item => ( 30 <div key={item.id}>{item.name}</div> 31 ))} 32 33 {loading && <div>加载中...</div>} 34 {error && <div>错误: {error}</div>} 35 </div> 36 ); 37};
高级用法
自定义 hasMore 判断
tsx1const MyComponent = () => { 2 const [total, setTotal] = useState(0); 3 4 const fetchData = async (page: number) => { 5 const response = await fetch(`/api/data?page=${page}`); 6 const result = await response.json(); 7 8 // 更新总数 9 setTotal(result.total); 10 11 return result.data; 12 }; 13 14 const { data, loading, containerRef, reset } = useInfiniteScroll(fetchData, { 15 // 根据总数判断是否还有更多数据 16 hasMoreChecker: (data) => data.length < total, 17 threshold: 50, // 距离底部50px时触发加载 18 }); 19 20 return ( 21 <div ref={containerRef} style={{ height: '400px', overflowY: 'auto' }}> 22 {data.map(item => ( 23 <div key={item.id}>{item.name}</div> 24 ))} 25 26 {loading && <div>加载中...</div>} 27 {data.length >= total && <div>没有更多数据了</div>} 28 </div> 29 ); 30};
条件加载
tsx1const MyComponent = () => { 2 const [userId, setUserId] = useState(''); 3 const [category, setCategory] = useState(''); 4 5 const fetchData = async (page: number) => { 6 const response = await fetch(`/api/data?page=${page}&userId=${userId}&category=${category}`); 7 return response.data; 8 }; 9 10 const { data, loading, containerRef, reset } = useInfiniteScroll(fetchData, { 11 enabled: !!(userId && category), // 只有在用户ID和分类都存在时才启用 12 deeps: [userId, category], // 当这些依赖变化时重新初始化 13 }); 14 15 // 当条件变化时重置数据 16 useEffect(() => { 17 if (userId && category) { 18 reset(); 19 } 20 }, [userId, category, reset]); 21 22 return ( 23 <div> 24 <select value={userId} onChange={(e) => setUserId(e.target.value)}> 25 <option value="">选择用户</option> 26 <option value="1">用户1</option> 27 <option value="2">用户2</option> 28 </select> 29 30 <select value={category} onChange={(e) => setCategory(e.target.value)}> 31 <option value="">选择分类</option> 32 <option value="A">分类A</option> 33 <option value="B">分类B</option> 34 </select> 35 36 <div ref={containerRef} style={{ height: '400px', overflowY: 'auto' }}> 37 {data.map(item => ( 38 <div key={item.id}>{item.name}</div> 39 ))} 40 {loading && <div>加载中...</div>} 41 </div> 42 </div> 43 ); 44};
手动控制加载
tsx1const MyComponent = () => { 2 const { 3 data, 4 loading, 5 containerRef, 6 loadMore, // 手动加载更多 7 reset, // 重置数据 8 currentPage // 当前页码 9 } = useInfiniteScroll(fetchData); 10 11 const handleRefresh = () => { 12 reset(); // 重置到第一页 13 setTimeout(() => { 14 loadMore(); // 手动触发加载 15 }, 0); 16 }; 17 18 return ( 19 <div> 20 <button onClick={handleRefresh}>刷新数据</button> 21 <button onClick={loadMore} disabled={loading}> 22 手动加载更多 (当前第{currentPage}页) 23 </button> 24 25 <div ref={containerRef} style={{ height: '400px', overflowY: 'auto' }}> 26 {data.map(item => ( 27 <div key={item.id}>{item.name}</div> 28 ))} 29 {loading && <div>加载中...</div>} 30 </div> 31 </div> 32 ); 33};
API
useInfiniteScroll(fetchMore, options?)
参数
fetchMore: (page: number) => Promise<T[]>
- 数据获取函数
- 参数:page - 页码(从1开始)
- 返回:Promise,解析为数据数组
options: UseInfiniteScrollOptions
- enabled?: boolean - 是否启用无限滚动,默认 true
- threshold?: number - 距离底部多少像素时触发加载,默认 100
- hasMoreChecker?: (data: T[], currentPage: number) => boolean - 自定义判断是否还有更多数据的函数
- deeps?: string[] - 依赖数组,当这些值都存在时才初始化加载
返回值
tsx1{ 2 data: T[]; // 当前已加载的所有数据 3 loading: boolean; // 是否正在加载 4 error: string | null; // 错误信息 5 containerRef: RefObject<HTMLDivElement>; // 容器引用,需要绑定到滚动容器 6 loadMore: () => Promise<void>; // 手动加载更多数据 7 reset: () => void; // 重置所有状态到初始值 8 currentPage: number; // 当前页码 9}
注意事项
- 容器引用:必须将 containerRef 绑定到可滚动的容器元素上
- 容器样式:确保容器有固定高度和 overflow-y: auto 样式
- 数据重置:当筛选条件变化时,记得调用 reset() 重置数据
- 错误处理:建议在 UI 中显示 error 状态
- 性能优化:Hook 内部使用了 refs 避免闭包问题,无需担心重复渲染
常见问题
Q: 为什么滚动到底部不触发加载?
A: 检查以下几点:
- 容器是否正确绑定了 containerRef
- 容器是否有固定高度和滚动样式
- enabled 选项是否为 true
- hasMoreChecker 是否返回 true
Q: 如何实现下拉刷新?
A: 调用 reset() 清空数据,然后手动调用 loadMore() 重新加载第一页
Q: 如何根据总数判断是否还有更多数据?
A: 使用 hasMoreChecker 选项:
tsx1hasMoreChecker: (data) => data.length < total
Q: 如何在条件变化时重新加载?
A: 使用 deeps 选项指定依赖,或在 useEffect 中调用 reset() 和 loadMore()
更新日志
- v1.0.0: 初始版本,支持基础无限滚动
- v1.1.0: 添加自定义 hasMoreChecker 支持
- v1.2.0: 添加 deeps 依赖控制和手动加载功能
评论讨论
使用 GitHub 账号登录即可参与讨论