基于HackerNews API实现批量请求边取数边分页渲染的问题
实现分批流式渲染的核心方案
当前逻辑无法实时渲染的根因是:所有ID的详情请求被统一打包到单个Promise.all中,必须等所有请求完成才会触发一次状态更新,自然无法做到边拉取边渲染。
核心调整逻辑
- 将全量500个ID按固定批次(可按需调整大小,匹配预期的每批10个)切分
- 逐批发起限流后的详情请求,每批的Promise全部完成后,立刻将该批次结果追加到状态数组触发重渲染,不等待后续批次
- 兼容原有前端分页逻辑:随着结果数组持续追加,分页组件的总页数会自动同步,无需改动现有分页计算规则
- 补充类型切换时的状态重置逻辑,避免新旧分类的数据混杂
- 修复原有代码中
setLoading(false)被同步执行、未等待请求完成就关闭加载态的bug
具体代码修改
1. request.js 核心改造
移除冗余的Promise包装,增加批次切分、逐批更新逻辑:
import axios from 'axios'; import BottleNeck from 'bottleneck' const limiter = new BottleNeck({ maxConcurrent: 1, minTime: 333 }) // 按需求配置单批拉取的ID数量 const BATCH_SIZE = 10; export const Request = (setResults, searchBarType, setLoading, shouldCancel) => { const searchBar = type => { const obj = { 'new': 'newstories', 'past': '', 'comments': 'user', 'ask': 'askstories', 'show': 'showstories', 'jobs': 'jobstories', 'top': 'topstories', 'best': 'beststories', 'user': 'user' } return obj[type] ? obj[type] : obj['new']; } let type = searchBar(searchBarType) const getData = () => { const options = type const API_URL = `https://hacker-news.firebaseio.com/v0/${options}.json?print=pretty`; // 移除冗余的new Promise包装,直接返回axios实例 return axios.get(API_URL); } const getIdFromData = (dataId) => { const API_URL = `https://hacker-news.firebaseio.com/v0/item/${dataId}.json?print=pretty`; return limiter.schedule(() => axios.get(API_URL)); } const runAsyncFunctions = async () => { setLoading(true) // 新请求触发时先清空历史结果,避免旧数据残留 setResults([]) const {data: allIds} = await getData() // 全量ID切分为固定大小的批次 const idBatches = [] for (let i = 0; i < allIds.length; i += BATCH_SIZE) { idBatches.push(allIds.slice(i, i + BATCH_SIZE)) } // 逐批执行请求,每批完成立刻更新状态 for (const batch of idBatches) { // 检查是否触发取消逻辑(比如用户切换了搜索类型) if (shouldCancel?.()) return const batchResults = await Promise.all( batch.map(id => getIdFromData(id).then(res => res.data)) ) // 每批拉完立刻追加到结果数组,触发组件重渲染 setResults(prev => [...prev, ...batchResults]) } setLoading(false) } runAsyncFunctions() }
2. App.js 适配调整
原有分页计算逻辑无需改动,仅需补充请求取消的竞态处理,避免旧请求干扰新状态:
import React, { useState, useEffect } from 'react'; import './App.css'; import { SearchBar } from './search-bar'; import { Results } from './results'; import { Request } from '../helper/request' import { Pagination } from './pagination'; function App() { const [results, setResults] = useState([]); const [searchBarType, setsearchBarType] = useState('news'); const [loading, setLoading] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [resultsPerPage] = useState(3); const handleClick = (e) => { const serachBarButtonId = e.target.id; setsearchBarType(serachBarButtonId) // 切换类型时重置到第一页 setCurrentPage(1) } useEffect(() => { // 增加取消标识,解决旧请求未完成时切换分类的竞态问题 let isCancelled = false Request( setResults, searchBarType, setLoading, () => isCancelled ) // 依赖更新时标记当前请求为取消 return () => { isCancelled = true } }, [searchBarType]) // 原有分页逻辑完全保留,随结果数组追加自动更新总页数 const indexOfLastResult = currentPage * resultsPerPage; const indexOfFirstResult = indexOfLastResult - resultsPerPage; const currentResults = results.slice(indexOfFirstResult, indexOfLastResult); const paginate = (pageNumber) => setCurrentPage(pageNumber); return ( <div className="App"> <SearchBar handleClick={handleClick} /> <Results results={currentResults} loading={loading} /> <Pagination resultsPerPage={resultsPerPage} totalResults={results.length} paginate={paginate} /> </div> ); } export default App;
实现后效果
- 第一批次10个ID的详情请求完成后,前端立刻渲染这10条数据,同时生成分页对应的页码,无需等待全量500条数据拉完
- 后续每批10个ID拉取完成,就自动追加到结果数组,分页总页数自动从少到多逐步更新,用户可以边加载边浏览已拿到的数据
- 原有限流逻辑完全保留,不会触发接口频率限制
- 解决了分类切换时的状态混杂、旧请求干扰新页面的竞态问题
小提示:如果需要调整单批拉取的粒度,只需修改
BATCH_SIZE的数值即可,无需改动其他逻辑。如果觉得每批10个的加载速度还可以再优化,也可以适当调大maxConcurrent的并发数,在接口允许的范围内提升拉取效率。
内容的提问来源于stack exchange,提问作者Seb
相关产品推荐
相关产品推荐

