为何useEffect钩子内代码在交叉观察时执行两次?求解决方案
React + TypeScript 无限滚动问题解答
1. 交叉时回调执行两次的原因及解决方案
原因
- React 18+开发环境的严格模式会强制组件挂载时重复执行两次useEffect(模拟卸载再挂载的场景),导致创建两个独立的IntersectionObserver实例,每个实例都会监听目标元素,交叉时触发两次回调。
- 目标元素初始可能已处于交叉状态,新创建的Observer会立即触发一次回调,叠加严格模式的重复创建,最终出现两次执行。
解决方案
- 清理Observer实例:在useEffect的返回函数中取消监听并断开Observer,避免重复实例残留。
- 添加加载锁:用状态标记是否正在加载,防止同一时间多次触发请求。
- 开发环境可临时关闭严格模式,但生产环境不会有此问题,更推荐前者的规范解法。
2. 代码放在useEffect内是否合理?
完全合理。IntersectionObserver的初始化属于组件挂载后的副作用操作,需要在组件卸载时清理,useEffect正是React设计用来处理这类场景的钩子。空依赖数组确保只在组件挂载时执行一次(严格模式下的重复执行会被清理逻辑抵消)。
3. 如何确保仅触发一次数据请求?
核心是通过加载状态锁避免重复请求,同时规范Observer的监听逻辑:
- 新增
isLoading状态,标记当前是否正在请求数据。 - 在Observer回调中,先判断
isLoading,若为true则直接返回,不执行后续逻辑。 - 请求开始时设置
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
相关产品推荐
相关产品推荐

