React JS下用扩展运算符合并数组实现AsyncSelect无限滚动求解
核心问题排查
- 选项返回结构错误:原代码
return [...fetchedData, allData]生成的是二维嵌套数组,已加载的allData是整个被当成单个数组元素塞进返回列表的,根本不是扁平的选项集合,React Select没法正确解析这类结构,会渲染出异常的选项内容。 - 页码更新时机完全错误:原代码里的
useEffect同时依赖scrolll和page两个值,组件首次挂载就会触发setPage(page + 1),不等用户操作就会持续把页码加到很高,发起大量错误页的无效请求。 - 滚动触发逻辑断层:原代码的
scroll方法里手动调用promiseOptions(),根本没有走React SelectloadOptions的标准返回流程,组件识别不到手动调用的结果,也不会把返回的数据追加到已渲染的选项列表里。 - 状态闭包陷阱:
filterData方法里拿到的page、allData都是组件上一次渲染的旧值,拼接数据的时候很容易出现重复、漏加的问题,拉取的页码和实际需要的页码对不上。 - 初始化阶段的拼接逻辑缺失:原代码只有
page > 1的时候才会把拉到的数据往allData里存,第一页加载完成的选项根本没被存入缓存,第一次滚动的时候直接拿空的allData做拼接,列表内容完全错乱。
适配后的实现方案
调整思路:
- 把页码、加载锁这类需要跨渲染周期保持最新值的变量存在
useRef里,避免闭包拿到旧状态 - 把“页码递增”的动作移到“成功拉取下一页数据之后”执行,完全去掉挂载时自动递增页码的副作用
- 所有返回给React Select的选项都做扁平化处理,保证数组结构是一维的
{label, value}对象集合 - 加简单的加载锁,避免滚动触底时连续触发重复请求
对应实现代码:
import React, { useState, useRef } from "react"; import AsyncSelect from "react-select/async"; const WithPromises = () => { // 存储所有已加载的扁平选项 const [allOptions, setAllOptions] = useState([]); // ref 存储当前待加载的页码,避免闭包旧值 const currentPageRef = useRef(1); // ref 存储加载状态锁,防止重复请求 const isLoadingRef = useRef(false); // 通用分页数据拉取方法 const requestPage = async (targetPage) => { const response = await fetch( `https://jsonplaceholder.typicode.com/todos?_limit=15&_page=${targetPage}` ); const rawList = await response.json(); // 接口返回空时代表没有更多数据,直接返回空数组 if (!rawList.length) return []; // 转换为组件要求的选项格式 return rawList.map(item => ({ label: item.title, value: item.title })); }; // loadOptions 标准回调 const handleLoadOptions = async () => { // 首次打开下拉时,加载第一页数据 if (allOptions.length === 0) { const firstPageData = await requestPage(currentPageRef.current); setAllOptions(firstPageData); currentPageRef.current += 1; return firstPageData; } // 后续分页加载走滚动回调逻辑,直接返回已缓存的所有选项 return allOptions; }; // 滚动到底部的回调 const handleMenuScrollToBottom = async () => { // 正在加载时直接返回,拦截重复请求 if (isLoadingRef.current) return; isLoadingRef.current = true; try { const nextPageData = await requestPage(currentPageRef.current); // 没有更多数据就直接结束加载 if (!nextPageData.length) return; // 把新拉取的选项拼到已有列表末尾,更新状态 setAllOptions(prevOptions => [...prevOptions, ...nextPageData]); currentPageRef.current += 1; } finally { // 无论请求成功失败,都释放加载锁 isLoadingRef.current = false; } }; return ( <AsyncSelect cacheOptions onMenuScrollToBottom={handleMenuScrollToBottom} isClearable isSearchable defaultOptions loadOptions={handleLoadOptions} /> ); }; export default WithPromises;
修复效果说明:
- 首次打开下拉会正常加载第一页15条选项
- 每次滚动条触底时,会自动拉取下一页15条数据,拼到现有选项列表的末尾,已加载的历史选项不会丢失
- 加载锁机制会拦截连续滚动触发的重复请求,不会出现同一个页码被多次拉取的问题
- 所有返回的选项都是一维扁平结构,组件可以正常解析渲染
内容的提问来源于stack exchange,提问作者Asking
相关产品推荐
相关产品推荐

