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

QueueAnim组件包裹后无限滚动失效问题排查

无限滚动结合rc-queue-anim后失效的问题排查与解决

我要实现一个带有进出动画和无限滚动功能的通知列表,用自定义Intersection Observer钩子触发无限滚动,采用ant-motion库的rc-queue-anim实现动画效果。不使用rc-queue-anim包裹时,无限滚动功能正常,但一旦用该组件包裹,无限滚动就不再触发,求排查问题。


无限滚动正常工作的代码

const NotificationList: React.FC<Props> = ( props: Props ) => {

  const { items, isLoading, isHistory, isError, loadMore, hasNext } = props;

  const sentryRef = useInfiniteScroll( {
    hasNextPage: hasNext,
    loadMore,
    isLoading,
    isDisabled: !!isError
  } );

  const notifications = useMemo( () => {
    if ( isLoading && hasNext ) return new Array( 5 ).fill( null ).map( ( _, index ) => <Notification status={ NotificationStatus.unread } type={ 'info' } title='' description='' actions={ [] } _id={ v4() } from='' loading={ isLoading } subject={ '' } key={ index } isHistory={ isHistory } /> );
    return items.map( ( notification, index ) =>
      <Notification
        { ...notification }
        ref={ index === items.length - 1 ? sentryRef : null }
        loading={ isLoading }
        isHistory={ isHistory }
        key={ notification._id }
      />
    );
  }, [ items, isLoading, hasNext ] );

  if ( isError ) return (
    <Result
      status="error"
      title="Network error"
      subTitle="Something went wrong! Please try again later."
    />
  );

  return (
    <Layout>
      <Content>
        { notifications }
      </Content>
    </Layout >
  );

};

添加rc-queue-anim后失效的代码

const NotificationList: React.FC<Props> = ( props: Props ) => {

  const { items, isLoading, isHistory, isError, loadMore, hasNext } = props;

  const sentryRef = useInfiniteScroll( {
    hasNextPage: hasNext,
    loadMore,
    isLoading,
    isDisabled: !!isError
  } );

  const notifications = useMemo( () => {
    if ( isLoading && hasNext ) return new Array( 5 ).fill( null ).map( ( _, index ) => <Notification status={ NotificationStatus.unread } type={ 'info' } title='' description='' actions={ [] } _id={ v4() } from='' loading={ isLoading } subject={ '' } key={ index } isHistory={ isHistory } /> );
    return items.map( ( notification, index ) =>
      <Notification
        { ...notification }
        ref={ index === items.length - 1 ? sentryRef : null }
        loading={ isLoading }
        isHistory={ isHistory }
        key={ notification._id }
      />
    );
  }, [ items, isLoading, hasNext ] );

  if ( isError ) return (
    <Result
      status="error"
      title="Network error"
      subTitle="Something went wrong! Please try again later."
    />
  );

  return (
    <Layout>
      <Content>
        <QueueAnim type={ [ 'right', 'left' ] } leaveReverse key="notification-list">
          { notifications }
        </QueueAnim>
      </Content>
    </Layout >
  );

};

自定义useInfiniteScroll钩子代码

import { useCallback, useRef } from "react";

interface Props {
  loadMore: () => void;
  hasNextPage?: boolean;
  isDisabled?: boolean;
  isLoading?: boolean;
}

export const useInfiniteScroll = ( props: Props ) => {
  const { isLoading, loadMore: next, hasNextPage, isDisabled } = props;
  const intObserver = useRef<IntersectionObserver>();
  const itemRef = useCallback( ( item: HTMLElement ) => {
    if ( isLoading || isDisabled ) return;

    if ( intObserver.current ) intObserver.current.disconnect();

    intObserver.current = new IntersectionObserver( items => {
      if ( items[ 0 ].isIntersecting && hasNextPage ) {
        console.log( "havava" );
        next();
      }
    } );
    if ( item ) intObserver.current.observe( item );
  }, [ hasNextPage, isLoading ] );

  return itemRef;
};

问题原因与解决方法

核心问题

  1. ref被拦截:rc-queue-anim会给每个子元素包裹一层动画容器,你传给Notification组件的ref会被这个容器捕获,无法绑定到真实的DOM节点上,导致IntersectionObserver监听失效。
  2. 钩子依赖不全:useInfiniteScroll里的useCallback依赖项缺失isDisabled和next,会引发闭包问题,导致状态变化时回调逻辑不更新。

修复步骤

1. 调整哨兵元素的绑定方式

不要把sentryRef绑定到Notification组件上,而是单独添加一个透明的哨兵元素,放在列表末尾:

const notificationsWithSentry = useMemo(() => {
  let list = [];
  if (isLoading && hasNext) {
    list = new Array(5).fill(null).map((_, index) => (
      <Notification
        status={NotificationStatus.unread}
        type="info"
        title=""
        description=""
        actions={[]}
        _id={v4()}
        from=""
        loading={isLoading}
        subject=""
        key={index}
        isHistory={isHistory}
      />
    ));
  } else {
    list = items.map((notification, index) => (
      <Notification
        {...notification}
        loading={isLoading}
        isHistory={isHistory}
        key={notification._id}
      />
    ));
  }
  // 添加哨兵元素,只有还有下一页且没报错时才显示
  if (hasNext && !isError) {
    list.push(<div ref={sentryRef} key="infinite-sentry" style={{ height: 1, opacity: 0 }} />);
  }
  return list;
}, [items, isLoading, hasNext, isError, sentryRef, isHistory]);

然后在QueueAnim里渲染这个新数组:

<QueueAnim type={['right', 'left']} leaveReverse key="notification-list">
  {notificationsWithSentry}
</QueueAnim>

2. 补全钩子的依赖项

修改useInfiniteScroll的useCallback依赖数组,加入isDisabled和next:

const itemRef = useCallback((item: HTMLElement) => {
  if (isLoading || isDisabled) return;

  if (intObserver.current) intObserver.current.disconnect();

  intObserver.current = new IntersectionObserver(items => {
    if (items[0].isIntersecting && hasNextPage) {
      console.log("havava");
      next();
    }
  });
  if (item) intObserver.current.observe(item);
}, [hasNextPage, isLoading, isDisabled, next]); // 补全缺失的依赖

原理说明

单独的哨兵元素不受rc-queue-anim的动画容器影响,ref能直接绑定到它的DOM节点上,IntersectionObserver可以正常监听其相交状态。补全依赖项则能避免闭包导致的逻辑不更新问题,确保钩子在相关状态变化时能正确重置监听。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 10:44:53