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; };
问题原因与解决方法
核心问题
- ref被拦截:rc-queue-anim会给每个子元素包裹一层动画容器,你传给Notification组件的ref会被这个容器捕获,无法绑定到真实的DOM节点上,导致IntersectionObserver监听失效。
- 钩子依赖不全: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
相关产品推荐
相关产品推荐

