You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 ])

  // ... 其余代码保持不变
}

额外优化点
  1. 避免重复添加元素:在处理可见元素时,增加了!onView.includes(key)判断,防止同一元素被多次添加到visible数组中。
  2. 清理观察者:在useEffect的返回函数中添加了观察者的清理逻辑,避免组件卸载后仍然存在内存泄漏。

内容的提问来源于stack exchange,提问作者Jose Miguel Arroyave

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 18:07:31