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

React中Intersection Observer状态更新异常问题求助

问题分析与解决方案:Intersection Observer无限滚动重复加载问题

问题现象

实现基于Intersection Observer的无限滚动时,预期依次显示Product 1到Product 6,但实际重复渲染Product 1;改用ref跟踪索引的实现方式则运行正常。

原因分析

核心问题是React闭包陷阱:

  • 第一个实现中,useEffect的依赖数组为空,意味着它只会在组件首次渲染时执行一次,内部的IntersectionObserver回调会捕获首次渲染时的ProductIds状态(初始为空数组)。
  • 每次触发updateProductIds时,函数内读取的ProductIds.length始终是0(闭包保留的是首次渲染时的旧状态值),所以每次取ids[0]也就是1,导致重复添加同一个id到状态中。
  • 而第二个实现用ref跟踪索引,ref.current是可变值,不受闭包捕获的影响,每次递增都能拿到最新的索引值,因此能正确加载后续id。

解决方案

提供三种可行的解决思路:

方案1:利用函数式更新获取最新状态

直接在setProductIds的函数式更新中,通过prevState获取最新的数组长度,避免依赖闭包中的旧状态:

const updateProductIds = useCallback(() => {
  setProductIds(prev => {
    const newId = ids[prev.length];
    if (newId) {
      return [...prev, newId];
    }
    return prev;
  });
}, [ids]);

同时调整useEffect的依赖数组,加入updateProductIds(用useCallback包裹避免不必要的重渲染):

useEffect(() => {
  const observer = new IntersectionObserver((entries) => {
    const loader = entries[0];
    if (loader.isIntersecting) {
      updateProductIds();
    }
  });

  if (loaderRef.current) {
    observer.observe(loaderRef.current);
  }

  return () => {
    if (loaderRef.current) {
      observer.unobserve(loaderRef.current);
    }
  };
}, [updateProductIds]);

方案2:使用ref跟踪当前索引

和你提供的可行实现思路一致,用ref存储当前要加载的索引,规避闭包问题:

const idRef = useRef(0);

useEffect(() => {
  const observer = new IntersectionObserver((entries) => {
    const loader = entries[0];
    if (loader.isIntersecting && idRef.current < ids.length) {
      setProductIds(prev => [...prev, ids[idRef.current]]);
      idRef.current++;
    }
  });

  if (loaderRef.current) {
    observer.observe(loaderRef.current);
  }

  return () => {
    if (loaderRef.current) {
      observer.unobserve(loaderRef.current);
    }
  };
}, [ids]);

方案3:调整useEffect依赖,直接依赖ProductIds

将ProductIds加入useEffect的依赖数组,每次状态更新后重新创建观察者,确保回调能捕获最新的ProductIds:

useEffect(() => {
  const observer = new IntersectionObserver((entries) => {
    const loader = entries[0];
    if (loader.isIntersecting) {
      updateProductIds();
    }
  });

  if (loaderRef.current) {
    observer.observe(loaderRef.current);
  }

  return () => {
    if (loaderRef.current) {
      observer.unobserve(loaderRef.current);
    }
  };
}, [ProductIds]);

function updateProductIds () {
  const newId = ids[ProductIds.length];
  if (newId) {
    setProductIds(prev => ([...prev, newId]))
  }
}

注意:这种方式会在每次ProductIds更新时重新创建并挂载观察者,性能略逊于前两种方案。

修正后的完整示例(方案1)

import { useState, useEffect, useRef, useCallback } from 'react';

function App() {
  const ids = [1, 2, 3, 4, 5, 6];
  const [ProductIds, setProductIds] = useState<number[]>([]);
  const loaderRef = useRef(null);

  const updateProductIds = useCallback(() => {
    setProductIds(prev => {
      const newId = ids[prev.length];
      if (newId) {
        return [...prev, newId];
      }
      return prev;
    });
  }, [ids]);

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      const loader = entries[0];
      if (loader.isIntersecting) {
        updateProductIds();
      }
    });

    if (loaderRef.current) {
      observer.observe(loaderRef.current);
    }

    return () => {
      if (loaderRef.current) {
        observer.unobserve(loaderRef.current);
      }
    };
  }, [updateProductIds]);

  return (
    <>
      {ProductIds.map((id: number) => (
        <div key={id} className="h-96 m-4 bg-green-900 text-white text-xl">
          {`Product ${id}`}
        </div>
      ))}

      <div
        ref={loaderRef} 
        className={`h-10 text-xl bg-gray-700 text-white`}
      >
        Loader
      </div>
    </>
  );
}

export default App;

内容的提问来源于stack exchange,提问作者temporary singh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 10:53:18