@react-google-maps/api自定义Overlay无法点击问题求助
问题描述
我基于React构建谷歌地图展示房产房源,视觉渲染正常,但用CustomOverlay组件替代默认标记后,绑定到div的onclick函数无法触发,控制台也没有输出日志。需要解决自定义Overlay的点击问题,使其和默认标记一样可点击;或者提供支持动态修改样式的替代方案。
CustomOverlay.js
import { useEffect, useRef } from 'react'; import { useGoogleMap } from '@react-google-maps/api'; const CustomOverlay = ({ marker, onMarkerClick }) => { const map = useGoogleMap(); const overlayView = useRef(null); const divRef = useRef(null); useEffect(() => { if (!map) return; const overlay = new window.google.maps.OverlayView(); overlay.onAdd = function () { const div = document.createElement('div'); const priceRounded = (marker.price / 1000000).toFixed(1); div.style.position = 'absolute'; div.style.transform = 'translate(-50%, -50%)'; div.style.cursor = 'pointer'; div.style.zIndex = '1'; div.innerHTML = ` <div class="price-label">£${priceRounded}m</div> <div class="fee-label">${marker.fee_offered}%</div> `; const onClickFunction = () => { onMarkerClick(marker); }; div.onclick = onClickFunction; divRef.current = div; overlay.getPanes().overlayLayer.appendChild(div); }; overlay.draw = function () { const div = divRef.current; const point = this.getProjection().fromLatLngToDivPixel(marker.position); if (point) { div.style.left = point.x + 'px'; div.style.top = point.y + 'px'; } }; overlay.onRemove = function () { if (divRef.current) { this.getPanes().overlayLayer.removeChild(divRef.current); } }; overlay.setMap(map); overlayView.current = overlay; return () => { overlayView.current.setMap(null); }; }, [map]); return null; }; export default CustomOverlay;
Map.js
import React, { useState, useEffect, useRef, useMemo, useCallback, } from 'react'; import { GoogleMap, useJsApiLoader, InfoWindow } from '@react-google-maps/api'; import { containerStyle, mapOptions } from './MapConfig'; import axios from 'axios'; import { getAccessToken } from '../../../../auth'; import './MapStyling/customoverlay.css'; import CustomOverlay from './MapComponents/CustomOverlay'; const api = axios.create({ baseURL: 'http://localhost:8000', }); api.interceptors.request.use( (config) => { const token = localStorage.getItem('access_token'); if (token) { config.headers['Authorization'] = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error), ); const Map = () => { const { isLoaded } = useJsApiLoader({ id: 'google-map-script', googleMapsApiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY, }); const [selectedMarker, setSelectedMarker] = useState(null); const [infoWindowVisible, setInfoWindowVisible] = useState(false); const [markers, setMarkers] = useState([]); const mapRef = useRef(null); const handleMarkerClick = useCallback( (marker) => { setInfoWindowVisible(false); setSelectedMarker(marker); setInfoWindowVisible(true); }, [setSelectedMarker, setInfoWindowVisible], ); useEffect(() => { const fetchData = async () => { const token = getAccessToken(); try { const response = await axios.get( process.env.REACT_APP_BACKEND_URL + 'listing/', { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'application/json', }, }, ); const markers = response.data.map((listing) => { let { lat, lng } = listing; let RestOfListing = { ...listing }; delete RestOfListing.lat; delete RestOfListing.lng; return { position: { lat, lng, }, ...RestOfListing, }; }); setMarkers(markers); } catch (error) { console.error(error); setMarkers([]); } }; fetchData(); }, []); const memoizedMarkers = useMemo(() => { return markers.map((marker) => ( <CustomOverlay key={marker.id} marker={marker} onMarkerClick={() => handleMarkerClick(marker)} /> )); }, [markers]); return isLoaded ? ( <GoogleMap mapContainerStyle={containerStyle} options={mapOptions} ref={mapRef} onClick={() => { setInfoWindowVisible(false); setSelectedMarker(null); }} > {memoizedMarkers} {infoWindowVisible && selectedMarker && ( <InfoWindow position={selectedMarker.position} onCloseClick={() => { setInfoWindowVisible(false); setSelectedMarker(null); }} > <div> <h4>Marker ID: {selectedMarker.id}</h4> </div> </InfoWindow> )} </GoogleMap> ) : null; }; export default Map;
customoverlay.css
.custom-marker { position: absolute; transform: translate(-25%, 0%); } .price-label { display: inline-block; background-color: rgba(22, 0, 87, 0.8); color: white; font-size: 14px; padding: 4px 8px; border-radius: 4px; } .fee-label { display: inline-block; background-color: #d62828; color: white; font-size: 14px; padding: 4px 8px; border-radius: 4px; margin-left: 4px; position: relative; top: -8px; translate: -125%; transform: translateY(-40%); z-index: -1; }
解决方案
问题核心原因
- 图层层级错误:将Overlay添加到
overlayLayer,该图层不专门处理鼠标事件,容易被地图其他元素遮挡 - 依赖项缺失:useEffect未监听
marker和onMarkerClick,props更新时无法重新绑定正确的点击回调 - 事件绑定方式不可靠:直接赋值
div.onclick容易被其他逻辑覆盖
修复后的CustomOverlay.js
import { useEffect, useRef } from 'react'; import { useGoogleMap } from '@react-google-maps/api'; const CustomOverlay = ({ marker, onMarkerClick }) => { const map = useGoogleMap(); const overlayView = useRef(null); const divRef = useRef(null); useEffect(() => { if (!map || !marker) return; const overlay = new window.google.maps.OverlayView(); overlay.onAdd = function () { const div = document.createElement('div'); const priceRounded = (marker.price / 1000000).toFixed(1); div.className = 'custom-marker'; div.style.cursor = 'pointer'; div.style.zIndex = '100'; div.innerHTML = ` <div class="price-label">£${priceRounded}m</div> <div class="fee-label">${marker.fee_offered}%</div> `; const onClickHandler = () => { onMarkerClick(marker); }; div.addEventListener('click', onClickHandler); divRef.current = { elem: div, handler: onClickHandler }; // 使用专门处理鼠标事件的图层 overlay.getPanes().overlayMouseTarget.appendChild(div); }; overlay.draw = function () { const div = divRef.current?.elem; const point = this.getProjection().fromLatLngToDivPixel(marker.position); if (div && point) { div.style.left = point.x + 'px'; div.style.top = point.y + 'px'; } }; overlay.onRemove = function () { if (divRef.current) { divRef.current.elem.removeEventListener('click', divRef.current.handler); this.getPanes().overlayMouseTarget.removeChild(divRef.current.elem); divRef.current = null; } }; overlay.setMap(map); overlayView.current = overlay; return () => { if (overlayView.current) { overlayView.current.setMap(null); overlayView.current = null; } }; }, [map, marker, onMarkerClick]); return null; }; export default CustomOverlay;
关键修复点
- 切换到
overlayMouseTarget图层:谷歌地图专门用于处理鼠标交互的图层,确保点击事件能被捕获 - 完善useEffect依赖项:监听
marker和onMarkerClick,确保props更新时重新创建Overlay并绑定正确回调 - 使用
addEventListener绑定事件:比直接赋值更稳定,同时在移除时解绑避免内存泄漏 - 提高z-index值:设置为100确保元素在地图其他图层之上
替代方案:使用自定义Marker组件
如果不想使用OverlayView,可直接用官方Marker组件,通过动态生成SVG图标实现自定义样式,天然支持点击事件:
import { Marker } from '@react-google-maps/api'; const CustomMarker = ({ marker, onMarkerClick }) => { const priceRounded = (marker.price / 1000000).toFixed(1); const customIcon = { url: `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="60" height="40"> <rect x="0" y="0" width="50" height="24" fill="rgba(22,0,87,0.8)" rx="4"/> <text x="25" y="16" font-family="Arial" font-size="14" fill="white" text-anchor="middle">£${priceRounded}m</text> <rect x="42" y="-8" width="28" height="24" fill="#d62828" rx="4"/> <text x="56" y="8" font-family="Arial" font-size="14" fill="white" text-anchor="middle">${marker.fee_offered}%</text> </svg>`, anchor: new window.google.maps.Point(30, 20), }; return ( <Marker position={marker.position} icon={customIcon} onClick={() => onMarkerClick(marker)} /> ); };
在Map.js中替换CustomOverlay为该组件即可,实现更简洁,无需手动管理Overlay生命周期。
Map.js额外优化
memoizedMarkers需加入handleMarkerClick作为依赖项,避免回调失效:
const memoizedMarkers = useMemo(() => { return markers.map((marker) => ( <CustomOverlay key={marker.id} marker={marker} onMarkerClick={() => handleMarkerClick(marker)} /> )); }, [markers, handleMarkerClick]);
内容的提问来源于stack exchange,提问作者Paul Wilson
相关产品推荐
相关产品推荐

