React中IntersectionObserver回调state为undefined及无限循环问题
React无限滚动(Intersection Observer)解决方案:避免state undefined与无限循环
问题根源
- 回调函数因闭包捕获旧值,导致未在依赖中加入
data时,回调里的data始终是初始的undefined; - 若将
data加入useEffect依赖,每次setData更新后都会重新创建Observer实例,触发重复监听与加载,进而引发无限循环。
解决方案1:使用函数式更新获取最新state
通过setData的函数式更新语法,无需在useEffect依赖中加入data,即可拿到最新状态,同时避免重复创建Observer。
import { useEffect, useRef, useState } from 'react'; const ThreeColumnGrid = () => { const [data, setData] = useState([]); const loaderRef = useRef(null); const observerRef = useRef(null); // 模拟接口请求替换成你的真实业务逻辑 const loadMore = async () => { const newItems = await fetch(`/api/grid-data?offset=${data.length}`).then(res => res.json()); // 函数式更新,确保拿到最新的state setData(prevData => [...prevData, ...newItems]); }; useEffect(() => { // 仅在组件挂载时创建一次Observer observerRef.current = new IntersectionObserver(entries => { const [entry] = entries; if (entry.isIntersecting) { loadMore(); } }); // 监听底部加载指示器 if (loaderRef.current) { observerRef.current.observe(loaderRef.current); } // 组件卸载时清理Observer,防止内存泄漏 return () => { observerRef.current?.disconnect(); }; }, []); // 依赖为空,只执行一次 return ( <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem', padding: '1rem' }}> {data.map(item => ( <div key={item.id} style={{ padding: '1rem', border: '1px solid #eee' }}> {item.content} </div> ))} {/* 触发滚动加载的指示器 */} <div ref={loaderRef} style={{ gridColumn: '1 / -1', padding: '2rem', textAlign: 'center' }}> 加载中... </div> </div> ); }; export default ThreeColumnGrid;
解决方案2:用useRef同步最新state引用
如果需要在回调中直接访问最新的data,可以用useRef同步state,避免闭包问题。
import { useEffect, useRef, useState } from 'react'; const ThreeColumnGrid = () => { const [data, setData] = useState([]); const dataRef = useRef(data); const loaderRef = useRef(null); const observerRef = useRef(null); // 同步state到ref,确保ref始终保存最新值 useEffect(() => { dataRef.current = data; }, [data]); const loadMore = async () => { const newItems = await fetch(`/api/grid-data?offset=${dataRef.current.length}`).then(res => res.json()); setData([...dataRef.current, ...newItems]); }; useEffect(() => { observerRef.current = new IntersectionObserver(entries => { const [entry] = entries; if (entry.isIntersecting) { loadMore(); } }); if (loaderRef.current) { observerRef.current.observe(loaderRef.current); } return () => { observerRef.current?.disconnect(); }; }, []); // 渲染部分同方案1 };
额外优化建议
- 添加加载状态:避免同一时间重复触发请求,比如在
loadMore中判断isLoading状态; - 处理无更多数据:当接口返回空数组时,停止监听加载指示器,并提示用户“没有更多数据”。
示例优化后的loadMore与加载指示器:
const [isLoading, setIsLoading] = useState(false); const [hasMore, setHasMore] = useState(true); const loadMore = async () => { if (isLoading || !hasMore) return; setIsLoading(true); try { const newItems = await fetch(`/api/grid-data?offset=${data.length}`).then(res => res.json()); if (newItems.length === 0) { setHasMore(false); } else { setData(prev => [...prev, ...newItems]); } } catch (err) { console.error('加载失败:', err); } finally { setIsLoading(false); } }; // 加载指示器渲染 <div ref={loaderRef} style={{ gridColumn: '1 / -1', padding: '2rem', textAlign: 'center' }}> {isLoading ? '加载中...' : hasMore ? '' : '没有更多数据了'} </div>
内容的提问来源于stack exchange,提问作者ygetarts
相关产品推荐
相关产品推荐

