React中IntersectionObserver回调内state变量未更新的无限滚动实现问题
问题:IntersectionObserver无限滚动中loading状态的竞态问题
我正尝试用IntersectionObserver实现无限滚动功能,逻辑是当回调触发时,先判断是否正在加载,如果是就取消本次调用。现在的实现函数是这样的:
const loadMore = useCallback(() => { if (loading) { return } setLoading(true); console.log('Loading'); setTimeout(() => { console.log('Finished'); setLoading(false); }, 5000); }, []);
但遇到了问题:当观察者再次调用loadMore时,如果已有加载操作在进行,loading状态有时还是旧值(false),导致重复触发setTimeout。要是把loading加入useCallback的依赖数组,又会因为函数内部修改loading值引发循环调用。
我试过用useRef控制加载状态,逻辑上能正常工作,但我还需要在JSX里用loading状态控制加载文本的显示/隐藏,而useRef变量没法在JSX中生效,同时维护两个几乎一样的变量又不够优雅。想问下有没有仅使用state就能解决这个问题的方案?
观察者相关代码如下:
useEffect(() => { const options = { root: null, rootMargin: '10px', threshold: 1.0 } const observer = new IntersectionObserver(loadMore, options); if (loaderRef && loaderRef.current) { observer.observe(loaderRef.current); } }, [loadMore]);
解决方案:只用State搞定竞态与循环调用问题
别担心,其实完全可以只用state解决这个问题,核心是调整状态更新的逻辑和依赖处理,避免闭包带来的旧值问题和循环调用,具体做法如下:
1. 改用函数式更新判断加载状态
把loadMore里的状态判断改成函数式更新,这样每次都能拿到最新的loading值,同时不用把loading加入useCallback的依赖数组,也就不会引发循环调用:
const loadMore = useCallback(() => { // 通过函数式更新获取最新的loading状态 setLoading(prevLoading => { // 如果已经在加载,直接返回当前状态,不执行后续逻辑 if (prevLoading) return prevLoading; // 开始加载流程 console.log('Loading'); setTimeout(() => { console.log('Finished'); setLoading(false); }, 5000); // 返回新的状态值 return true; }); }, []); // 这里不需要依赖loading,函数式更新帮我们拿到最新值
2. 完善Observer的销毁逻辑
另外,你的useEffect需要加上销毁观察者的清理函数,防止组件卸载后还触发回调,而且现在loadMore的依赖是空数组,useEffect只会执行一次,不会重复创建观察者:
useEffect(() => { const options = { root: null, rootMargin: '10px', threshold: 1.0 }; const observer = new IntersectionObserver(loadMore, options); const targetElement = loaderRef.current; if (targetElement) { observer.observe(targetElement); } // 组件卸载或依赖变化时,停止观察目标元素 return () => { if (targetElement) { observer.unobserve(targetElement); } }; }, [loadMore]);
原理说明
- 函数式更新
setLoading(prev => ...)会让React把最新的状态值传给我们,彻底避免了闭包捕获旧loading值的问题,再也不会出现“明明在加载却判断成false”的情况。 loadMore的依赖数组是空的,所以它只会在组件初始化时创建一次,useEffect也只会执行一次,不会引发循环调用。loading作为state变量,完全可以直接在JSX中用来控制加载文本的显示/隐藏,不需要额外维护ref变量,代码更简洁优雅。
这样调整后,你的无限滚动逻辑就正常了,而且完全只用state实现,满足你的需求~
内容的提问来源于stack exchange,提问作者Gustavo Fabro
相关产品推荐
相关产品推荐

