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

为何useEffect钩子内代码在交叉观察时执行两次?求解决方案

React + TypeScript 无限滚动问题解答

1. 交叉时回调执行两次的原因及解决方案

原因

  • React 18+开发环境的严格模式会强制组件挂载时重复执行两次useEffect(模拟卸载再挂载的场景),导致创建两个独立的IntersectionObserver实例,每个实例都会监听目标元素,交叉时触发两次回调。
  • 目标元素初始可能已处于交叉状态,新创建的Observer会立即触发一次回调,叠加严格模式的重复创建,最终出现两次执行。

解决方案

  • 清理Observer实例:在useEffect的返回函数中取消监听并断开Observer,避免重复实例残留。
  • 添加加载锁:用状态标记是否正在加载,防止同一时间多次触发请求。
  • 开发环境可临时关闭严格模式,但生产环境不会有此问题,更推荐前者的规范解法。

2. 代码放在useEffect内是否合理?

完全合理。IntersectionObserver的初始化属于组件挂载后的副作用操作,需要在组件卸载时清理,useEffect正是React设计用来处理这类场景的钩子。空依赖数组确保只在组件挂载时执行一次(严格模式下的重复执行会被清理逻辑抵消)。

3. 如何确保仅触发一次数据请求?

核心是通过加载状态锁避免重复请求,同时规范Observer的监听逻辑:

  1. 新增isLoading状态,标记当前是否正在请求数据。
  2. 在Observer回调中,先判断isLoading,若为true则直接返回,不执行后续逻辑。
  3. 请求开始时设置isLoading为true,请求完成后设为false,同时可暂时取消监听,等数据更新后重新监听(避免滚动时频繁触发)。

4. 加入data到依赖数组导致执行混乱的原因

当data加入依赖数组后,每次setData更新数据时,useEffect都会重新执行,创建新的IntersectionObserver实例并监听目标元素。这会导致:

  • 多个Observer实例同时监听同一元素,交叉时触发多次回调。
  • 每次data更新都重建Observer,回调执行次数完全失控。

解决方法

  • 不要把data加入依赖数组,使用setData的函数式更新(你当前的prev => prev.concat(add)就是正确写法,无需依赖data)。
  • 把用到的add加入依赖数组,避免捕获旧的状态值。

修改后的示例代码

//PracticePage.tsx
import styled from "styled-components";
import { useEffect, useRef, useState } from "react";

const PracticePage = () => {
  const [add, setAdd] = useState([1]);
  const [data, setData] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
  const [isLoading, setIsLoading] = useState(false);
  const targetRef = useRef<HTMLDivElement>(null);
  const observerRef = useRef<IntersectionObserver | null>(null);

  const testFetch = (delay = 1000) =>
    new Promise((res) => setTimeout(res, delay));

  useEffect(() => {
    // 创建Observer实例并存储到ref,避免重复创建
    observerRef.current = new IntersectionObserver(
      async ([entry], observer) => {
        if (entry.isIntersecting && !isLoading) {
          setIsLoading(true);
          await testFetch();
          console.log("intersect!");
          setData((prev) => prev.concat(add));
          setIsLoading(false);
          // 暂时取消监听,避免滚动时频繁触发
          observer.unobserve(entry.target);
        }
      },
      {
        root: null,
        rootMargin: "0px 0px 0px 0px",
        threshold: 0,
      }
    );

    const target = targetRef.current;
    if (target) {
      observerRef.current.observe(target);
    }

    // 清理函数:组件卸载时断开Observer
    return () => {
      if (observerRef.current) {
        observerRef.current.disconnect();
      }
    };
  }, [add, isLoading]);

  // 数据更新后重新监听目标元素
  useEffect(() => {
    if (observerRef.current && targetRef.current && !isLoading) {
      observerRef.current.observe(targetRef.current);
    }
  }, [data, isLoading]);

  return (
    <>
      <Container>
        {data.map((d, index) => (
          <Items key={index}>{d}</Items>
        ))}
        <Target ref={targetRef}>target</Target>
      </Container>
    </>
  );
};

const Container = styled.div`
  width: 100vw;
  height: 500px;
  background-color: gray;
  overflow-y: scroll;
`;
const Items = styled.div`
  width: 50vw;
  height: 50px;
  margin: 10px auto;
  background-color: yellow;
`;
const Target = styled.div`
  width: 50vw;
  height: 50px;
  margin: 10px auto;
  color: white;
  background-color: black;
`;

export default PracticePage;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 22:39:17