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

React Places Autocomplete:点击建议项误判为外部点击求助

React Places Autocomplete 外部点击检测异常修复

问题描述

我使用React Places Autocomplete生成地点建议,希望强制用户必须选择其中一个建议地点,因此添加了代码检测建议容器外的点击。但实际效果不符合预期:即使点击建议列表里的span元素,也会被判定为点击在容器外部,导致输入框被清空。

问题代码片段

建议容器部分:

<div ref={suggestionsContainerRef}>
  {suggestions.map(suggestion => {
    const className = suggestion.active ? 'suggestion-item--active' : 'suggestion-item';
    const style = suggestion.active
                ? { backgroundColor: '#fafafa', cursor: 'pointer' }
                : { backgroundColor: '#ffffff', cursor: 'pointer' };
    return (
      <div {...getSuggestionItemProps(suggestion, { className, style })}  >
        <span >{suggestion.description}</span>
      </div>
    )
  })}
</div>

完整组件代码:

type LocationInputProps = {
    id: string
    label: string
    placeholder: string
}

const LocationInput = ({id, label, placeholder}: LocationInputProps) => {

  const [address, setAddress] = useState<string>('');
  const [lat, setLat] = useState<number>();
  const [lon, setLon] = useState<number>();
  const [selectedAddress, setSelectedAddress] = useState(false);
  const suggestionsContainerRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    document.addEventListener('click', handleDocumentClick);
    return () => {
      document.removeEventListener('click', handleDocumentClick);
    };
  }, []);

  const handleDocumentClick = (event: MouseEvent) => {
    if (
      suggestionsContainerRef.current &&
      !suggestionsContainerRef.current.contains(event.target as Node)
    ) {
        console.log('click is not within container')
        setAddress(''); 
    } 
  };

  return(
    <PlacesAutocomplete
        value={address}
        onChange={(address) => {setAddress(address)}}
        onSelect={(address) => {
          geocodeByAddress(address)
            .then(results => getLatLng(results[0]))
            .then(latLng => {console.log('Success', latLng)
        setLat(latLng.lat);
        setLon(latLng.lng);
        setSelectedAddress(true);
    })
            .catch(error => {console.error('Error', error)
        setSelectedAddress(false)});
       
          setAddress(address);
          
        }}
        searchOptions={{componentRestrictions: {country: 'US'}}}
      >
        {({ getInputProps, suggestions, getSuggestionItemProps }) => (
          <div>
          
              <div className="grid w-9/12 items-center gap-1.5">
      <Label>{label}</Label>
      <Input  id={id} placeholder={placeholder} {...getInputProps({
                className: 'location-search-input'
              })}/>
    </div>
    
            <div ref={suggestionsContainerRef}>
              {suggestions.map(suggestion => {
                const className = suggestion.active ? 'suggestion-item--active' : 'suggestion-item';
                const style = suggestion.active
                            ? { backgroundColor: '#fafafa', cursor: 'pointer' }
                            : { backgroundColor: '#ffffff', cursor: 'pointer' };
                return (
                  <div {...getSuggestionItemProps(suggestion, { className, style })}  >
                    <span >{suggestion.description}</span>
                  </div>
                )
              })}
            </div>
          </div>
        )}
      </PlacesAutocomplete>
  )
}

export default LocationInput

问题原因

问题出在事件执行顺序和DOM节点卸载:
当点击建议项的span时,React的合成事件onSelect会先执行,此时组件重新渲染,建议列表因输入框值变为选中地址而清空,对应的span节点被卸载。随后原生document点击事件触发,event.target指向已卸载的span,而suggestionsContainerRef.current指向已清空的容器div,contains方法判断div不包含已卸载的span,因此误判为外部点击,执行清空输入框的逻辑。

修复方案

通过两步解决问题:

  1. 使用useRef标记刚完成的选择操作,避免事件循环误判。
  2. 增加输入框点击判断,防止点击输入框时清空内容。

修复后的完整代码

type LocationInputProps = {
    id: string
    label: string
    placeholder: string
}

const LocationInput = ({id, label, placeholder}: LocationInputProps) => {

  const [address, setAddress] = useState<string>('');
  const [lat, setLat] = useState<number>();
  const [lon, setLon] = useState<number>();
  const [selectedAddress, setSelectedAddress] = useState(false);
  const suggestionsContainerRef = useRef<HTMLDivElement | null>(null);
  const inputRef = useRef<HTMLInputElement | null>(null);
  // 标记是否刚完成地址选择,避免事件循环误判
  const justSelectedRef = useRef(false);

  useEffect(() => {
    document.addEventListener('click', handleDocumentClick);
    return () => {
      document.removeEventListener('click', handleDocumentClick);
    };
  }, []);

  const handleDocumentClick = (event: MouseEvent) => {
    // 如果刚完成选择,跳过本次判断
    if (justSelectedRef.current) return;
    
    const target = event.target as Node;
    const isClickInsideContainer = suggestionsContainerRef.current?.contains(target);
    const isClickInsideInput = inputRef.current?.contains(target);

    // 既不在建议容器内,也不在输入框内,才清空输入
    if (!isClickInsideContainer && !isClickInsideInput) {
        console.log('click is not within container or input')
        setAddress(''); 
    } 
  };

  return(
    <PlacesAutocomplete
        value={address}
        onChange={(address) => {setAddress(address)}}
        onSelect={(address) => {
          // 标记刚完成选择
          justSelectedRef.current = true;
          // 在下一次事件循环重置标记
          setTimeout(() => {
            justSelectedRef.current = false;
          }, 0);

          geocodeByAddress(address)
            .then(results => getLatLng(results[0]))
            .then(latLng => {
              console.log('Success', latLng)
              setLat(latLng.lat);
              setLon(latLng.lng);
              setSelectedAddress(true);
            })
            .catch(error => {
              console.error('Error', error)
              setSelectedAddress(false)
            });
       
          setAddress(address);
        }}
        searchOptions={{componentRestrictions: {country: 'US'}}}
      >
        {({ getInputProps, suggestions, getSuggestionItemProps }) => (
          <div>
            <div className="grid w-9/12 items-center gap-1.5">
              <Label>{label}</Label>
              <Input 
                ref={inputRef}
                id={id} 
                placeholder={placeholder} 
                {...getInputProps({ className: 'location-search-input' })}
              />
            </div>
            
            <div ref={suggestionsContainerRef}>
              {suggestions.map(suggestion => {
                const className = suggestion.active ? 'suggestion-item--active' : 'suggestion-item';
                const style = suggestion.active
                            ? { backgroundColor: '#fafafa', cursor: 'pointer' }
                            : { backgroundColor: '#ffffff', cursor: 'pointer' };
                return (
                  <div {...getSuggestionItemProps(suggestion, { className, style })}>
                    <span>{suggestion.description}</span>
                  </div>
                )
              })}
            </div>
          </div>
        )}
      </PlacesAutocomplete>
  )
}

export default LocationInput

解释

  • justSelectedRef:用useRef保存选择状态,避免闭包导致的状态更新延迟。在onSelect中标记为true,并通过setTimeout在下一次事件循环重置,确保document点击事件能识别出本次是有效选择,跳过清空逻辑。
  • inputRef:给输入框添加ref,确保点击输入框时不会触发清空操作,符合正常输入预期。
  • 修改后的handleDocumentClick:同时判断点击是否在建议容器或输入框内,只有都不在时才清空输入框,彻底避免误判。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 05:53:09