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

@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;
}

解决方案

问题核心原因

  1. 图层层级错误:将Overlay添加到overlayLayer,该图层不专门处理鼠标事件,容易被地图其他元素遮挡
  2. 依赖项缺失:useEffect未监听marker和onMarkerClick,props更新时无法重新绑定正确的点击回调
  3. 事件绑定方式不可靠:直接赋值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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 18:34:58