Ant Select组件搜索字符时下拉弹窗偶现不显示问题求助
Ant Design Select 远程搜索字符时弹窗不显示问题排查与修复
问题背景
基于React开发的自定义Ant Select组件,支持两种模式:
- 直接传入
options数据展示(功能正常) - 通过
select_url从服务器拉取数据(大部分功能正常,但搜索字符时下拉弹窗有时不显示,搜索数字则完全正常)
已确认:调用setOptions后_options状态中存在数据,但弹窗仍不展示。
组件代码如下:
import React, {useEffect, useState} from "react"; import {Select, Spin} from "antd"; import {SelectOption} from "../../@types/app"; interface CustomSelectProps { options?: SelectOption[], placeholder?: string, select_url?: string } const CustomSelect: React.FC<CustomSelectProps> = ( { options, placeholder = "انتخاب کنید", select_url } ) => { const [_options, setOptions] = useState<undefined | SelectOption[]>(options); const [nextPageUrl, setNextPageUrl] = useState<string | null>(null); const [loading, setLoading] = useState<boolean>(false); useEffect(() => { if (select_url) { search() } else if (options && !select_url) { setOptions(options); } else { console.log("specify data. SELECT"); } }, []); const search = async (input: string = '') => { if (!select_url) return; setLoading(true); setOptions(() => []); const url = `${select_url}?search=${input}&page=1`; try { const response = await fetch(url); if (!response.ok) { return new Error(`Network response was not ok (${response.status})`); } const data = await response.json(); setNextPageUrl(data.next); setOptions(prevOptions => [...prevOptions, ...data.data]); } catch (error) { console.error('Error fetching data:', error); } finally { setLoading(false); } }; const handlePopupScroll = async (e: React.UIEvent<HTMLDivElement>) => { if (!select_url) return; const popupContainer = e.currentTarget; const isAtEndOfScroll = popupContainer.scrollTop + popupContainer.clientHeight === popupContainer.scrollHeight; if (isAtEndOfScroll && nextPageUrl && !loading) { try { const res = await (await fetch(nextPageUrl)).json(); setNextPageUrl(res.next); const {data} = res; setOptions((prevState) => { return [...prevState, ...data]; }); } catch (e) { console.log(e); } finally { setLoading(false); } } } // filter options based on user input in client mode. const handleFilterOption = (input: string, option?: SelectOption) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase()); return ( <Select options={_options} placeholder={placeholder} showSearch={true} filterOption={select_url ? undefined : handleFilterOption} onSearch={search} notFoundContent={loading ? <Spin size="small"/> : null} virtual={true} onPopupScroll={handlePopupScroll} > </Select> ); } export default CustomSelect;
服务器返回数据格式:
[ { "label": "test", "value": "value", "key": "1" } ]
问题排查
核心问题出在虚拟滚动(virtual={true})与远程数据更新的配合逻辑:
- 搜索字符时,
search函数先同步执行setOptions(() => [])清空选项,此时虚拟滚动容器高度收缩为0; - 异步请求完成后更新
_options,但Ant Select的虚拟滚动组件未检测到选项数组变化,容器高度未重新计算,导致弹窗看似未显示; - 搜索数字时服务器返回更快,或数据量/结构差异让虚拟滚动容器及时更新高度,因此表现正常。
此外还有几个潜在问题:
useEffect依赖数组为空,父组件传入的options变化时组件无法同步更新;search函数先清空再添加的时序,可能导致虚拟滚动内部状态异常;handlePopupScroll未设置loading状态,易触发重复请求。
修复方案
1. 修复虚拟滚动状态同步
给Select组件添加key属性,搜索时更新key强制组件重新渲染:
const [searchKey, setSearchKey] = useState(0); const search = async (input: string = '') => { if (!select_url) return; setLoading(true); setSearchKey(prev => prev + 1); // 更新key触发重新渲染 // ... 其余逻辑不变 } // Select组件添加key <Select key={searchKey} // ... 其余属性不变 />
2. 修正useEffect依赖
将options加入依赖数组,确保父组件更新时同步:
useEffect(() => { if (select_url) { search() } else if (options) { setOptions(options); } else { console.log("specify data. SELECT"); } }, [select_url, options]); // 添加options依赖
3. 优化search函数状态更新
直接用请求结果替换选项,避免先清空再添加的时序问题:
const search = async (input: string = '') => { if (!select_url) return; setLoading(true); setSearchKey(prev => prev + 1); const url = `${select_url}?search=${input}&page=1`; try { const response = await fetch(url); if (!response.ok) { throw new Error(`Network response was not ok (${response.status})`); } const data = await response.json(); setNextPageUrl(data.next); setOptions(data.data); // 直接替换为新数据 } catch (error) { console.error('Error fetching data:', error); setOptions([]); // 请求失败时清空 } finally { setLoading(false); } };
4. 完善滚动加载的loading状态
在滚动加载前设置loading,防止重复请求:
const handlePopupScroll = async (e: React.UIEvent<HTMLDivElement>) => { if (!select_url) return; const popupContainer = e.currentTarget; const isAtEndOfScroll = popupContainer.scrollTop + popupContainer.clientHeight === popupContainer.scrollHeight; if (isAtEndOfScroll && nextPageUrl && !loading) { setLoading(true); // 先设置loading try { const res = await (await fetch(nextPageUrl)).json(); setNextPageUrl(res.next); const {data} = res; setOptions((prevState) => [...prevState, ...data]); } catch (e) { console.log(e); } finally { setLoading(false); } } }
修复后的完整组件代码
import React, {useEffect, useState} from "react"; import {Select, Spin} from "antd"; import {SelectOption} from "../../@types/app"; interface CustomSelectProps { options?: SelectOption[], placeholder?: string, select_url?: string } const CustomSelect: React.FC<CustomSelectProps> = ( { options, placeholder = "انتخاب کنید", select_url } ) => { const [_options, setOptions] = useState<undefined | SelectOption[]>(options); const [nextPageUrl, setNextPageUrl] = useState<string | null>(null); const [loading, setLoading] = useState<boolean>(false); const [searchKey, setSearchKey] = useState(0); useEffect(() => { if (select_url) { search() } else if (options) { setOptions(options); } else { console.log("specify data. SELECT"); } }, [select_url, options]); const search = async (input: string = '') => { if (!select_url) return; setLoading(true); setSearchKey(prev => prev + 1); const url = `${select_url}?search=${input}&page=1`; try { const response = await fetch(url); if (!response.ok) { throw new Error(`Network response was not ok (${response.status})`); } const data = await response.json(); setNextPageUrl(data.next); setOptions(data.data); } catch (error) { console.error('Error fetching data:', error); setOptions([]); } finally { setLoading(false); } }; const handlePopupScroll = async (e: React.UIEvent<HTMLDivElement>) => { if (!select_url) return; const popupContainer = e.currentTarget; const isAtEndOfScroll = popupContainer.scrollTop + popupContainer.clientHeight === popupContainer.scrollHeight; if (isAtEndOfScroll && nextPageUrl && !loading) { setLoading(true); try { const res = await (await fetch(nextPageUrl)).json(); setNextPageUrl(res.next); const {data} = res; setOptions((prevState) => [...prevState, ...data]); } catch (e) { console.log(e); } finally { setLoading(false); } } } const handleFilterOption = (input: string, option?: SelectOption) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase()); return ( <Select key={searchKey} options={_options} placeholder={placeholder} showSearch={true} filterOption={select_url ? undefined : handleFilterOption} onSearch={search} notFoundContent={loading ? <Spin size="small"/> : null} virtual={true} onPopupScroll={handlePopupScroll} /> ); } export default CustomSelect;
内容的提问来源于stack exchange,提问作者joseph.o
相关产品推荐
相关产品推荐

