基于React-Query实现无限滚动后,如何用React-Window实现列表虚拟化?
解决React-Query无限滚动结合React-Window列表虚拟化的问题
核心思路
要解决DOM臃肿导致的性能问题,需将原一次性渲染所有文章的逻辑替换为React-Window的虚拟化列表——仅渲染当前视口内的文章,同时保留React-Query的分页数据管理能力。关键步骤为:扁平化已加载的文章数组、用React-Window组件替代原生map渲染、调整无限滚动触发逻辑适配虚拟化场景。
具体实现步骤
- 安装react-window
npm install react-window # 或 yarn add react-window
- 修改代码实现虚拟化
以下是整合后的完整代码,关键修改点已标注:
import { useInfiniteQuery } from '@tanstack/react-query'; import { VariableSizeList as List } from 'react-window'; // 适配不同高度的文章项 import { useRef, useMemo } from 'react'; const API_KEY = '你的API密钥'; async function fetchPage({ pageParam = 1 }) { const response = await fetch( `https://newsapi.org/v2/everything?q=India&pageSize=30&page=${pageParam}&apiKey=${API_KEY}` ); return response.json(); } function App() { const { isLoading, data, fetchNextPage, isFetchingNextPage, hasNextPage } = useInfiniteQuery(["news"], fetchPage, { getNextPageParam: (lastPage, allPages) => { const nextPage = allPages.length + 1; return lastPage.articles.length !== 0 ? nextPage : undefined; }, }); // 1. 扁平化所有已加载的文章数组 const allArticles = useMemo(() => { return data?.pages?.flatMap(page => page.articles) || []; }, [data]); // 2. 缓存文章高度,避免重复计算 const articleHeights = useRef({}); // 3. 获取单篇文章高度(渲染后缓存实际高度) const getArticleHeight = (index) => { return articleHeights.current[index] || 300; // 默认高度兜底 }; // 4. 渲染单篇文章的回调函数 const renderArticle = ({ index, style }) => { const article = allArticles[index]; if (!article) return null; const handleRef = (el) => { if (el && !articleHeights.current[index]) { articleHeights.current[index] = el.offsetHeight; // 高度更新后重置列表布局,保证滚动准确性 listRef.current?.resetAfterIndex(index); } }; return ( <div style={style} ref={handleRef} className="news_article"> <section> <img src={article.urlToImage || 'https://via.placeholder.com/400x200'} alt={article.title} /> </section> <h2> <a href={article.url} target="_blank" rel="noopener noreferrer">{article.title}</a> </h2> <p>{article.description || '暂无描述'}</p> <p>{article.author || '未知作者'}</p> </div> ); }; const listRef = useRef(null); // 5. 监听列表渲染事件,触发下一页加载 const handleItemsRendered = ({ visibleStopIndex }) => { // 当滚动到倒数第5条时,自动加载下一页 if (visibleStopIndex >= allArticles.length - 5 && hasNextPage && !isFetchingNextPage) { fetchNextPage(); } }; if (isLoading) { return <p>Loading ...</p>; } return ( <> <div className="app" style={{ height: '100vh', overflow: 'auto' }}> {/* 虚拟化列表核心组件 */} <List ref={listRef} height={window.innerHeight} itemCount={allArticles.length} itemSize={getArticleHeight} onItemsRendered={handleItemsRendered} width="100%" > {renderArticle} </List> {/* 底部加载状态提示 */} <div className="loader"> {isFetchingNextPage ? "Loading..." : hasNextPage ? "" : "No more news"} </div> </div> </> ); } export default App;
关键修改说明
- 扁平化数组:通过
flatMap将多页的articles合并为一个连续数组,满足React-Window对列表数据的格式要求。 - VariableSizeList:针对新闻文章高度不一致的场景,使用可变高度列表组件,避免固定高度导致的布局错乱;若文章高度固定,可替换为更轻量的
FixedSizeList。 - 高度缓存机制:通过ref缓存每篇文章的实际渲染高度,后续滚动时直接复用,同时在高度更新后重置列表布局,保证滚动位置准确。
- 滚动触发加载:利用React-Window的
onItemsRendered事件,在滚动接近列表末尾时自动触发下一页请求,替代原IntersectionObserver方案,更贴合虚拟化列表的特性。 - 样式适配:给父容器设置固定高度和滚动属性,确保虚拟化列表能正确计算可视区域范围。
注意事项
- 给
img标签设置默认图片地址和alt属性,避免图片加载失败时的布局异常。 - 处理
description、author等可能为空的字段,避免渲染空内容影响体验。 - 列表容器必须设置明确高度,否则React-Window无法计算可视区域,虚拟化功能失效。
内容的提问来源于stack exchange,提问作者Moumita Dhar
相关产品推荐
相关产品推荐

