React实现无限滚动时window.addEventListener更新state仅能到2
无限滚动实现的核心问题根因
- 空依赖
useEffect的闭包陷阱:注册在依赖为[]的effect内部的scrollHandler,只捕获了组件首次渲染时的scrollNumber初始值1。这个监听器从组件挂载后就不会随组件重渲染更新内部引用,哪怕后续scrollNumber实际值已经变成2,监听器逻辑里拿到的还是闭包存储的旧值,每次执行setScrollNumber(scrollNumber +1)本质都是基于旧值1计算,永远只能得到2,无法继续累加。 - 逻辑冗余:两个独立的
useEffect都在请求同一个分页接口,首次挂载时会触发两次完全重复的请求,没有统一的数据流入口。 - 滚动触发条件过严:硬判断
元素底部位置 === 窗口可视区高度,滚动过程中因为像素取整、滚动动画插值,几乎很难精准匹配完全相等的临界值,实际触发成功率极低。 - 数组截取存在副作用:用
result.splice()截取分页数据会直接修改接口返回的原数组本身,多次请求时因为原数组被篡改,取到的分页数据长度会不符合预期。 - 缺少防重锁:滚动触发更新时,没有拦截短时间内的重复触发,容易导致分页参数被多次累加、重复发起请求。
对应修正逻辑
- 滚动监听器内部的分页更新改用
setScrollNumber(prev => prev +1)的函数式写法,不依赖闭包捕获的旧值,从根上解决闭包捕获过期state的问题。 - 删除首次挂载时冗余的请求effect,只保留由
scrollNumber驱动的数据加载逻辑,避免重复请求。 - 调整滚动触发阈值,在距离可视区底部100px时就提前触发加载,兼顾滚动流畅度和触发成功率。
- 数组截取改用无副作用的
slice方法,保证分页数据截取的准确性。 - 新增
isLoading状态锁,请求过程中不再触发新的分页更新,避免重复拉取。
修正后可直接运行的完整代码
import React, { useEffect, useRef, useState } from 'react'; const Container: React.FC<{}> = () => { const containerRef = useRef<HTMLDivElement | null>(null); const [scrollNumber, setScrollNumber] = useState<number>(1); const [posts, setPosts] = useState<any[]>([]); const [isLoading, setIsLoading] = useState(false); // 分页数据加载逻辑:仅跟随scrollNumber变化执行 useEffect(() => { const loadData = async () => { setIsLoading(true); try { const req = await fetch('https://jsonplaceholder.typicode.com/posts'); const result = await req.json(); // 用无副作用的slice截取对应页数的数据 const pst = result.slice(0, 10 * scrollNumber); setPosts(pst); } finally { setIsLoading(false); } }; loadData(); }, [scrollNumber]); // 滚动监听器逻辑:组件挂载时绑定,卸载时解绑 useEffect(() => { const scrollHandler = () => { // 请求中直接拦截,避免重复触发 if (!containerRef.current || isLoading) return; const rect = containerRef.current.getBoundingClientRect(); // 距离底部100px即触发下一页加载 if (rect.bottom <= window.innerHeight + 100) { // 函数式更新,不依赖闭包捕获的旧state值 setScrollNumber(prev => prev + 1); } }; window.addEventListener('scroll', scrollHandler); return () => window.removeEventListener('scroll', scrollHandler); }, [isLoading]); return ( <div ref={containerRef}> {posts.map(post => ( <div key={post.id}> <br /> <h1>{post.title}</h1> <p>{post.body}</p> <code>{post.userId}</code> <br /><br /> <hr /> </div> ))} {isLoading && <div style={{ textAlign: 'center', padding: '24px' }}>内容加载中...</div>} </div> ); }; export default Container;
内容的提问来源于stack exchange,提问作者Ek4m
相关产品推荐
相关产品推荐

