React-Query+虚拟列表组件内滚动抖动及卡片消失问题求助
React Query无限加载+React Virtual虚拟化滚动卡顿闪烁问题修复
问题描述
使用react-query实现无限加载信息流,结合@tanstack/react-virtual对MUI卡片做虚拟化处理。需求是应用高度固定为100vh,禁止全局滚动,仅让承载信息流的组件局部滚动,因此给该组件添加了overflowY: 'scroll'属性。
添加属性后出现异常:滚动卡顿、频繁闪烁,滚动位置计算错乱,最终所有卡片消失。移除overflowY: 'scroll'允许全局滚动后,问题消失。
相关代码
LogCards组件(承载信息流)
const LogCards = ({ pageLimit }: LogCardsProps) => { const { data: logs, fetchNextPage, isFetchingNextPage, isLoading, } = useInfiniteLogsQuery(pageLimit); const { ref, inView } = useInView(); const allLogs = useMemo(() => logs?.pages.flatMap((log) => log.results), [logs]) || []; const parentRef = useRef<HTMLDivElement>(null); const rowVirtualizer = useVirtualizer({ count: allLogs.length, getScrollElement: useCallback(() => parentRef.current, []), estimateSize: useCallback(() => 300, []), overscan: 2, gap: 8, }); useEffect(() => { if (inView) fetchNextPage(); }, [fetchNextPage, inView]); return !isLoading ? ( <Box ref={parentRef} sx={{ overflowY: 'scroll' }}> <div style={{ height: `${rowVirtualizer.getTotalSize()}px` }}> {allLogs && rowVirtualizer.getVirtualItems().map((log, index) => { const LogCardData = allLogs[log.index]; console.log(LogCardData); if (!LogCardData) return <span key={`no card index ${index}`}>No cards</span>; return ( <LogCard key={LogCardData.id} data-index={log.index} ref={rowVirtualizer.measureElement} log={LogCardData} /> ); })} </div> <Box sx={{ height: '20px' }} ref={ref}> {isFetchingNextPage && <LinearProgress sx={{ m: '2px' }} />} </Box> </Box> ) : ( <LinearLoader /> ); };
LogsList组件(承载LogCards)
const LogsList = () => { const [pageLimit, setPageLimit] = useState(25); return ( <Paper sx={{ display: 'flex', flexDirection: 'column', height: '100vh', px: 1, borderRadius: { xs: '4px', md: 0 }, }} > <Typography variant="h5" textAlign="center" sx={{ py: 2 }}> List of unsolved logs </Typography> <LogsLimitButtons pageLimit={pageLimit} setPageLimit={setPageLimit} /> <LogCards pageLimit={pageLimit} /> </Paper> ); };
应用整体布局
const LaunchDesktop = () => { return ( <Grid2 container spacing={1}> <Grid2 size={{ xs: 12, md: 8 }} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', }} > <SosForm /> </Grid2> <Grid2 size={{ xs: 12, md: 4 }}> <LogsList /> </Grid2> </Grid2> ); };
问题原因与修复方案
核心原因
- 滚动容器无固定高度:
LogCards的滚动Box仅设置了overflowY: 'scroll',未限制高度。父组件是flex布局,滚动容器会被无限拉伸,导致虚拟滚动器无法正确计算视口范围和滚动位置。 - 虚拟项未正确定位:虚拟滚动的子元素缺少绝对定位,无法被虚拟滚动器精确控制位置,导致布局错乱。
- 无限加载触发逻辑适配问题:
useInView监听的是全局视口,而非局部滚动容器的视口,导致加载时机判断错误,进一步加剧滚动异常。
修复步骤
1. 给滚动容器设置固定高度
修改LogCards中的滚动Box样式,利用flex布局特性让它占据父容器剩余空间:
<Box ref={parentRef} sx={{ overflowY: 'scroll', flex: 1, height: 0 }}>
flex:1让组件占据LogsList中除标题和按钮外的剩余空间,height:0强制组件使用flex分配的空间,避免高度计算异常。
2. 给虚拟项添加定位
修改内部容器为相对定位,给每个LogCard添加绝对定位和位置样式,让虚拟滚动器控制卡片位置:
<div style={{ height: `${rowVirtualizer.getTotalSize()}px`, position: 'relative', width: '100%', }} >
<LogCard key={LogCardData.id} data-index={log.index} ref={rowVirtualizer.measureElement} log={LogCardData} style={{ position: 'absolute', top: `${log.start}px`, left: 0, right: 0, height: `${log.size}px`, }} />
3. 优化无限加载触发逻辑
改用虚拟滚动器的虚拟项位置判断加载时机,替代useInView,适配局部滚动场景:
useEffect(() => { const lastItem = rowVirtualizer.getVirtualItems().pop(); if (lastItem && !isFetchingNextPage && lastItem.index >= allLogs.length - 5) { fetchNextPage(); } }, [rowVirtualizer.getVirtualItems(), allLogs.length, fetchNextPage, isFetchingNextPage]);
当最后一个可见虚拟项的索引接近总数据量时(提前5个),触发下一页加载,避免视口判断错误。
修复后的完整LogCards代码
const LogCards = ({ pageLimit }: LogCardsProps) => { const { data: logs, fetchNextPage, isFetchingNextPage, isLoading, } = useInfiniteLogsQuery(pageLimit); const allLogs = useMemo(() => logs?.pages.flatMap((log) => log.results), [logs]) || []; const parentRef = useRef<HTMLDivElement>(null); const rowVirtualizer = useVirtualizer({ count: allLogs.length, getScrollElement: useCallback(() => parentRef.current, []), estimateSize: useCallback(() => 300, []), overscan: 2, gap: 8, }); useEffect(() => { const lastItem = rowVirtualizer.getVirtualItems().pop(); if (lastItem && !isFetchingNextPage && lastItem.index >= allLogs.length - 5) { fetchNextPage(); } }, [rowVirtualizer.getVirtualItems(), allLogs.length, fetchNextPage, isFetchingNextPage]); return !isLoading ? ( <Box ref={parentRef} sx={{ overflowY: 'scroll', flex: 1, height: 0 }}> <div style={{ height: `${rowVirtualizer.getTotalSize()}px`, position: 'relative', width: '100%', }} > {rowVirtualizer.getVirtualItems().map((log) => { const LogCardData = allLogs[log.index]; if (!LogCardData) return null; return ( <LogCard key={LogCardData.id} data-index={log.index} ref={rowVirtualizer.measureElement} log={LogCardData} style={{ position: 'absolute', top: `${log.start}px`, left: 0, right: 0, height: `${log.size}px`, }} /> ); })} </div> <Box sx={{ height: '40px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> {isFetchingNextPage && <LinearProgress sx={{ width: '90%' }} />} </Box> </Box> ) : ( <LinearLoader /> ); };
内容的提问来源于stack exchange,提问作者magrega
相关产品推荐
相关产品推荐

