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
相关产品推荐
相关产品推荐

