You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React实现无限滚动时触底停止加载、仅显示15个元素如何解决

无限滚动失效的核心原因
  • 滚动触发条件精度要求过高:原代码通过window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight做到底判断,受浏览器默认边距、滚动条惯性、像素取整、组件自带边距等影响,实际滚动到底时,两边的数值通常存在1~10像素的偏差,很难达到完全相等的状态,当列表累计渲染15个元素后,页面高度达到一定值,这个偏差就会导致触发条件永远不成立。
  • 滚动监听存在闭包陷阱:原代码直接在组件渲染逻辑中给window.onscroll赋值,每次组件重渲染都会覆盖旧的监听函数,且没有在组件卸载时解绑监听;监听回调会捕获创建时的旧page值,容易出现状态更新不一致的问题。
  • 状态更新逻辑存在闭包依赖:原useEffect中直接展开外部捕获的state数组做更新,没有使用函数式更新获取最新的列表值,容易在多次请求触发时出现数据覆盖、拼接错乱的问题。
  • 缺少请求状态锁:没有标记当前是否存在进行中的请求,滚动到底时会连续触发多次page更新,造成接口重复请求,甚至出现状态跳变。
可稳定运行的修正实现
import { useState, useEffect } from 'react';

const PAGE_SIZE = 5;
const INITIAL_PAGE = 0;

export default function PassengerList() {
  const [list, setList] = useState([]);
  const [page, setPage] = useState(INITIAL_PAGE);
  const [isLoading, setIsLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);

  // 统一管理滚动监听的绑定与解绑
  useEffect(() => {
    const handlePageScroll = () => {
      // 距离底部100px即触发加载,避免像素精度问题
      const scrollTriggerOffset = 100;
      const visibleHeight = window.innerHeight;
      const scrolledDistance = document.documentElement.scrollTop;
      const totalPageHeight = document.documentElement.offsetHeight;
      const isReachTriggerPoint = totalPageHeight - (visibleHeight + scrolledDistance) <= scrollTriggerOffset;

      // 已无更多数据、正在加载、未到触发点,直接返回
      if (!isReachTriggerPoint || isLoading || !hasMore) return;
      setPage(prevPage => prevPage + 1);
    };

    window.addEventListener('scroll', handlePageScroll);
    // 组件卸载时移除监听,避免内存泄漏
    return () => window.removeEventListener('scroll', handlePageScroll);
  }, [isLoading, hasMore]);

  // 分页数据拉取逻辑
  useEffect(() => {
    // 首次加载已完成则跳过重复请求
    if (page === INITIAL_PAGE && list.length > 0) return;
    setIsLoading(true);
    fetch(`https://api.instantwebtools.net/v1/passenger?page=${page}&size=${PAGE_SIZE}`)
      .then(res => res.json())
      .then(result => {
        // 用函数式更新获取最新列表,避免闭包拿旧值
        setList(prevList => [...prevList, ...result.data]);
        // 返回数据不足单页大小,说明已经加载完全部内容
        if (result.data.length < PAGE_SIZE) {
          setHasMore(false);
        }
      })
      .catch(err => console.error('数据加载异常', err))
      .finally(() => setIsLoading(false));
  }, [page]);

  return (
    <>
      {list.map(item => (
        <div key={item.id} className="container">
          <h4>{item.id}</h4>
          <h4>{item.name}</h4>
        </div>
      ))}
      {isLoading && <div style={{textAlign: 'center', padding: 16}}>数据加载中...</div>}
      {!hasMore && <div style={{textAlign: 'center', padding: 16}}>已加载全部内容</div>}
    </>
  );
}
关键调整说明
  • 把原有的严格相等判断改为距离底部固定阈值触发,既避免像素精度问题,也能提前加载下一页内容,优化滚动流畅度。
  • 用标准事件监听API替代直接给onscroll赋值,在组件卸载时做解绑处理,从机制上解决闭包捕获旧状态的问题,同时避免内存泄漏。
  • 增加isLoading、hasMore两个状态锁,拦截重复的加载触发,到达数据末尾时自动停止翻页。
  • 所有列表状态更新都采用函数式写法,直接获取上一次的状态值做计算,避免闭包导致的状态更新错乱。
  • 把列表渲染的key从数组索引替换为数据自带的唯一id,减少DOM复用带来的渲染异常。

内容的提问来源于stack exchange,提问作者Pranav

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.25 19:24:00