You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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})与远程数据更新的配合逻辑:

  1. 搜索字符时,search函数先同步执行setOptions(() => [])清空选项,此时虚拟滚动容器高度收缩为0;
  2. 异步请求完成后更新_options,但Ant Select的虚拟滚动组件未检测到选项数组变化,容器高度未重新计算,导致弹窗看似未显示;
  3. 搜索数字时服务器返回更快,或数据量/结构差异让虚拟滚动容器及时更新高度,因此表现正常。

此外还有几个潜在问题:

  • 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 16:49:56