BlockCoder
前端开发··12 分钟

React Hooks 封装一个列表触底刷新

封装一个列表触底刷新的 React Hooks,基于传入的container容器div去监听滚动到底部。

#React#Hooks#JavaScript#List

一个功能强大、灵活的 React 无限滚动 Hook,支持页码分页、自定义加载条件和外部状态控制。

特性

  • 🚀 页码分页:使用标准的页码参数,从第1页开始
  • 🎯 灵活控制:支持外部函数判断是否还有更多数据
  • 🔄 状态管理:内置加载状态、错误处理和数据重置
  • 📱 性能优化:使用 refs 避免闭包问题,支持被动滚动监听
  • 🛠 依赖控制:支持依赖条件,只有在满足条件时才初始化加载
  • 🎨 TypeScript:完整的 TypeScript 支持

安装

bash
1# 该 Hook 位于项目内部
2import { useInfiniteScroll } from '@/pages/data-sync/hooks';

基础用法

tsx
1import 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 判断

tsx
1const 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};

条件加载

tsx
1const 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};

手动控制加载

tsx
1const 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[] - 依赖数组,当这些值都存在时才初始化加载

返回值

tsx
1{
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}

注意事项

  1. 容器引用:必须将 containerRef 绑定到可滚动的容器元素上
  2. 容器样式:确保容器有固定高度和 overflow-y: auto 样式
  3. 数据重置:当筛选条件变化时,记得调用 reset() 重置数据
  4. 错误处理:建议在 UI 中显示 error 状态
  5. 性能优化:Hook 内部使用了 refs 避免闭包问题,无需担心重复渲染

常见问题

Q: 为什么滚动到底部不触发加载?

A: 检查以下几点:

  • 容器是否正确绑定了 containerRef
  • 容器是否有固定高度和滚动样式
  • enabled 选项是否为 true
  • hasMoreChecker 是否返回 true

Q: 如何实现下拉刷新?

A: 调用 reset() 清空数据,然后手动调用 loadMore() 重新加载第一页

Q: 如何根据总数判断是否还有更多数据?

A: 使用 hasMoreChecker 选项:

tsx
1hasMoreChecker: (data) => data.length < total

Q: 如何在条件变化时重新加载?

A: 使用 deeps 选项指定依赖,或在 useEffect 中调用 reset()loadMore()

更新日志

  • v1.0.0: 初始版本,支持基础无限滚动
  • v1.1.0: 添加自定义 hasMoreChecker 支持
  • v1.2.0: 添加 deeps 依赖控制和手动加载功能

评论讨论

使用 GitHub 账号登录即可参与讨论