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

React中Intersection Observer引发数组重复等问题求助

问题描述

基于Intersection Observer实现React无限滚动后遇到三个问题:

  • 展开、折叠再重新展开由hideSection控制的区块时,数组数据重复,引发大量React重复key错误;
  • 偶尔滚动时page值异常,单次触发页码增加2而非1;
  • 不进行展开/折叠操作时,也偶尔出现重复key错误。

所有记录均有唯一ID,API分页数据经验证正确,调整useEffect和useInfiniteScroll后无效,求解决建议。


相关代码

父组件

export function Section({ combine }: SectionProps) {
    const [hideSection, setHideSection] = useState(false);
    const { ref, position } = useInfiniteScroll();
    const { serverData, isLoading, totalCount } = useFetchData(position, hideSection);

    return (
        <main>
            {!combine &&
                <>
                    <Group
                        totalCount={totalCount}
                        key="completed"
                        title="Data"
                        data={serverData}
                        hidden={hideSection}
                        setHidden={setHideSection}
                        isLoading={isLoading} />
                    <div ref={ref}></div>
                </>}
        </main>
    )
}

数据获取Hook

export function useFetchData(page: number, isHidden: boolean) {
    const [serverData, setServerData] = useState<any>([]);
    const [totalCount, setTotalCount] = useState<number>(0);
    const [position, setPosition] = useState<number>(page);
    const { data, isLoading } = useApiGetData().usePaging(
        {
            pageNumber: page,
            pageSize: 20,
        },
        !!page,
        { refetchOnWindowFocus: false }
    );
    useEffect(() => {
        setPosition(page);
        if (!isHidden && position === page) {
            setServerData((prev: any[]) => prev.concat(data?.data));
        }
        setTotalCount(data?.totalRecords || 0);
    }, [isHidden, data, position]);

    return { isLoading, serverData, totalCount };
}

Intersection Observer Hook

export function useInfiniteScroll() {
    const [position, setPosition] = useState(0);
    const ref = useRef(null);

    const handleObserver = useCallback((entries) => {
        entries.forEach((entry: any) => {
            if (entry.intersectionRatio > 0) {
              setPosition((prev) => prev + 1);
            }
          });
    }, []);

    useEffect(() => {
        const option = {
            root: null,
            rootMargin: '0px',
            threshold: 1
        };

        const observer = new IntersectionObserver(handleObserver, option);

        if (ref.current) observer.observe(ref.current);

    }, [handleObserver]);

    return { ref, position };
}

解决方案

1. 解决展开折叠后数据重复问题

问题根源是hideSection切换时未重置数据,且Hook内部状态与传入参数不同步:

  • 在useFetchData中监听isHidden变化,隐藏时清空数据:
    useEffect(() => {
        if (isHidden) {
            setServerData([]);
        }
    }, [isHidden]);
    
  • 移除useFetchData内部的position状态,直接使用传入的page参数,避免状态不同步;同时添加重复数据检查,防止重复拼接:
    export function useFetchData(page: number, isHidden: boolean) {
        const [serverData, setServerData] = useState<any>([]);
        const [totalCount, setTotalCount] = useState<number>(0);
        const { data, isLoading } = useApiGetData().usePaging(
            { pageNumber: page, pageSize: 20 },
            !!page,
            { refetchOnWindowFocus: false }
        );
    
        // 隐藏时重置数据
        useEffect(() => {
            if (isHidden) setServerData([]);
        }, [isHidden]);
    
        // 分页数据拼接逻辑
        useEffect(() => {
            if (!isHidden && data?.data?.length && page > 0) {
                // 检查是否已存在该页数据
                const hasDuplicate = serverData.some(item => 
                    data.data.some(newItem => newItem.id === item.id)
                );
                if (!hasDuplicate) {
                    setServerData(prev => [...prev, ...data.data]);
                }
            }
            if (data?.totalRecords) setTotalCount(data.totalRecords);
        }, [page, data, isHidden, serverData]);
    
        return { isLoading, serverData, totalCount };
    }
    

2. 解决page值异常增加的问题

因Intersection Observer可能多次触发回调,需添加加载状态判断,避免加载中重复触发页码增加:

  • 修改useInfiniteScroll,接收isLoading参数控制页码更新:
    export function useInfiniteScroll(isLoading: boolean) {
        const [position, setPosition] = useState(0);
        const ref = useRef(null);
    
        const handleObserver = useCallback((entries) => {
            entries.forEach((entry: any) => {
                // 仅在元素进入视口且非加载状态时更新页码
                if (entry.isIntersecting && !isLoading) {
                    setPosition(prev => prev + 1);
                }
            });
        }, [isLoading]);
    
        useEffect(() => {
            const option = { root: null, rootMargin: '0px', threshold: 1 };
            const observer = new IntersectionObserver(handleObserver, option);
            const currentRef = ref.current;
    
            if (currentRef) observer.observe(currentRef);
    
            // 清理函数避免内存泄漏
            return () => {
                if (currentRef) observer.unobserve(currentRef);
            };
        }, [handleObserver]);
    
        return { ref, position };
    }
    
  • 父组件中传递isLoading给Hook:
    const { ref, position } = useInfiniteScroll(isLoading);
    

3. 解决重复key错误

除了防止数据重复,必须确保列表渲染时使用唯一ID作为key:

  • 在Group组件内部渲染列表项时,严格使用每条数据的唯一ID(如item.id):
    // Group组件内列表渲染示例
    {data.map(item => (
        <ListItem key={item.id} {...item} />
    ))}
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 13:36:03