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

React SearchBar组件下拉项onClick失效:focus状态控制显示的问题

问题原因

核心问题是input的onBlur事件触发时机早于下拉项的onClick:当你点击下拉列表的<li>时,浏览器会先触发input的blur事件,把searchFocus设为false,这时候下拉面板会被立即卸载,<li>的点击事件根本没机会执行。

解决方案

下面提供几种可行的修复方式,按推荐程度排序:

方法1:监听全局点击,判断点击区域(最健壮)

通过ref标记组件容器,监听全局点击事件,只有点击组件外部时才关闭下拉面板:

type Props = {
    onClickResult: (result: string, key: number) => void
    placeHolder: string
    results: string[]
    holdValue: boolean
}

const SearchBar = (props: Props): JSX.Element => {
    const [search, setSearch] = useState<string>('')
    const [searchFocus, setSearchFocus] = useState<boolean>(false)
    // 创建ref标记组件容器
    const searchComponentRef = useRef<HTMLDivElement>(null);

    useEffect(() => {
        const handleClickOutside = (event: MouseEvent) => {
            // 点击区域不在组件内时,关闭面板
            if (searchComponentRef.current && !searchComponentRef.current.contains(event.target as Node)) {
                setSearchFocus(false);
            }
        };
        // 添加全局监听
        document.addEventListener('mousedown', handleClickOutside);
        // 组件卸载时移除监听
        return () => document.removeEventListener('mousedown', handleClickOutside);
    }, []);

    function handleChooseResult(result: string, key: number) {
        props.onClickResult(result, key)
        props.holdValue ? setSearch(result) : setSearch('')
    }

    return (
        <div id="search-component" ref={searchComponentRef}>
            <div className="search-bar">
                <FaSearch id="search-icon"/>
                <input 
                    type="text" 
                    placeholder={props.placeHolder} 
                    value={search} 
                    onChange={(e) => setSearch(e.target.value)}
                    onFocus={() => setSearchFocus(true)}
                    // 移除原来的onBlur
                />
            </div>
            <div className="search-results">
                { searchFocus &&
                    <ul>
                        {props.results.filter((result) =>
                            // 优化过滤逻辑,避免substring的长度问题
                            result.toLowerCase().startsWith(search.toLowerCase())        
                        ).map((result, key) => 
                            <li key={key} onClick={() => handleChooseResult(result, key)}>{result}</li>
                        )}
                    </ul>
                }
            </div>
        </div>
    )
}

方法2:用onMouseDown替代onClick

因为mousedown事件会在blur之前触发,所以能保证点击逻辑先执行:

// 只修改<li>的事件
<li key={key} onMouseDown={() => handleChooseResult(result, key)}>{result}</li>

注意:如果需要阻止input失去焦点,可以在handleChooseResult里添加event.preventDefault(),但一般不需要,选择后关闭面板是合理的。

方法3:延迟onBlur的状态更新

给onBlur加一个短延迟,让onClick有足够时间执行:

<input 
    type="text" 
    placeholder={props.placeHolder} 
    value={search} 
    onChange={(e) => setSearch(e.target.value)}
    onFocus={() => setSearchFocus(true)}
    onBlur={() => setTimeout(() => setSearchFocus(false), 100)}
/>

这个方法简单但不够优雅,延迟时间需要调整,极端情况下可能还是会出问题。

额外代码优化建议
  1. 不要把props存到本地state:你当前用useState存了props.results,但父组件更新results时,组件内的results不会同步更新,直接用props.results即可。
  2. 优化过滤逻辑:原来的substring写法在search长度大于result时会报错,改成startsWith更安全,也符合前缀匹配的搜索场景;如果需要模糊匹配,用includes即可。

内容的提问来源于stack exchange,提问作者Caue

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 21:04:56