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

三个属性的意义如下:

  1. scrollHeight 是容器的总高度,包括不可见的滚动区域
  2. scrollTop 是已经滚动完成的高度,也就是滚出去的上半部分高度
  3. 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);   // 强制从第一页加载
};

前面提到的 isLoadingisRefreshing 就是为了区分 loadMorerefresh

为什么不复用 isLoading,原因有几个:

  1. 要区分数据的赋值逻辑,loadMore 是拼接,refresh 是重设
  2. 单状态管理会导致竞态条件,刷新和加载都共同维护 isLoading,会增加复杂度,很可能出现双方通过异步先后修改 isLoading 的情况
  3. 能显示不同的提示语

初始化#

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();
  // ...刷新逻辑
};
触底分页加载模版代码
https://blog.erio.work/posts/触底分页加载模版代码/
作者
Dupfioire
发布于
2020-03-27
许可协议
CC BY-NC-SA 4.0