React中useState与IntersectionObserver配合使用的问题排查
问题原因:闭包导致的状态捕获问题
你的问题核心在于React闭包特性:当IntersectionObserver在初始useEffect中创建时,它绑定的onInteraction回调函数捕获了组件初始渲染时的visible状态(即空数组[])。后续组件重新渲染时,虽然visible状态已经更新,但IntersectionObserver仍然使用的是初始版本的回调函数,因此始终只能访问到初始的visible值。
解决方案1:使用
setVisible的函数式更新 React的setState支持传入一个函数,该函数会接收当前最新的状态值作为参数。这种方式可以彻底避开闭包问题,因为我们不再依赖回调函数捕获的visible变量,而是直接从React获取最新状态。
修改后的代码如下:
function _IntersectionObserverTest_( props ){ const { data } = props const scrollDiv = useRef( null ) const itemsRef = useRef( [] ) const [ visible, setVisible ] = useState([]) const visibilityThreshold = 0.5 // 使用useCallback包裹回调,确保依赖变化时回调能更新 const onInteraction = useCallback((entries, opts) => { setVisible(prevVisible => { let onView = [...prevVisible]; entries.forEach(e => { const key = e.target.getAttribute('itemKey'); const t = e.intersectionRatio; if (t < visibilityThreshold) { // 移除不可见元素 onView = onView.filter(k => k !== key); } else { // 添加可见元素,避免重复 if (!onView.includes(key)) { onView.push(key); } } }); // 去重后返回新状态 return [...new Set(onView)]; }); }, [visibilityThreshold]); useEffect(() => { itemsRef.current = itemsRef.current.slice(0, props.data.length); }, [props.data]); useEffect( () => { const observer = new IntersectionObserver(onInteraction, { root: scrollDiv.current, threshold: visibilityThreshold, }); // 监听所有元素 itemsRef.current.forEach(el => { if (el) observer.observe(el); }); // 组件卸载时清理观察者 return () => { itemsRef.current.forEach(el => { if (el) observer.unobserve(el); }); observer.disconnect(); }; }, [ onInteraction, scrollDiv, itemsRef, visibilityThreshold ]) // ... 其余代码保持不变 }
解决方案2:使用Ref保存最新回调
另一种方法是用useRef保存最新的onInteraction回调函数,这样IntersectionObserver触发时,始终调用的是最新版本的回调(能访问到最新的visible状态)。
修改后的代码如下:
function _IntersectionObserverTest_( props ){ const { data } = props const scrollDiv = useRef( null ) const itemsRef = useRef( [] ) const [ visible, setVisible ] = useState([]) const visibilityThreshold = 0.5 // 用Ref保存最新的回调函数 const onInteractionRef = useRef(); // 每次渲染更新Ref,确保回调始终是最新的 onInteractionRef.current = (entries, opts) => { let onView = [...visible]; entries.forEach(e => { const key = e.target.getAttribute('itemKey'); const t = e.intersectionRatio; if (t < visibilityThreshold) { onView = onView.filter(k => k !== key); } else { if (!onView.includes(key)) { onView.push(key); } } }); setVisible([...new Set(onView)]); }; useEffect(() => { itemsRef.current = itemsRef.current.slice(0, props.data.length); }, [props.data]); useEffect( () => { const observer = new IntersectionObserver((entries, opts) => { // 调用Ref中保存的最新回调 onInteractionRef.current(entries, opts); }, { root: scrollDiv.current, threshold: visibilityThreshold, }); itemsRef.current.forEach(el => { if (el) observer.observe(el); }); return () => { itemsRef.current.forEach(el => { if (el) observer.unobserve(el); }); observer.disconnect(); }; }, [ scrollDiv, itemsRef, visibilityThreshold ]) // ... 其余代码保持不变 }
额外优化点
- 避免重复添加元素:在处理可见元素时,增加了
!onView.includes(key)判断,防止同一元素被多次添加到visible数组中。 - 清理观察者:在
useEffect的返回函数中添加了观察者的清理逻辑,避免组件卸载后仍然存在内存泄漏。
内容的提问来源于stack exchange,提问作者Jose Miguel Arroyave
相关产品推荐
相关产品推荐

