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,因此误判为外部点击,执行清空输入框的逻辑。
修复方案
通过两步解决问题:
- 使用
useRef标记刚完成的选择操作,避免事件循环误判。 - 增加输入框点击判断,防止点击输入框时清空内容。
修复后的完整代码
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
相关产品推荐
相关产品推荐

