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

Google Maps React API多边形onSave延迟更新问题求助

问题

我正在使用Google Maps React API开发多边形渲染与编辑功能,目前已实现以下功能:

  • 成功渲染后端返回的GeoJSON数据;
  • 可识别多边形顶点,弹出菜单删除顶点后,多边形能即时更新,状态变量也同步变化。

但遇到onSave函数的问题:每次mouseup触发onSave时,控制台日志可正常输出,但新的GeoJSON结构无法即时同步到状态,需触发第二次才会更新。现附上代码:

export function MapComponents() {
  const t = useTranslations();
  const { fieldDataV2, setPolygon, polygon } = useFieldDetailsContext();
  const { map } = useLayoutProvider();
  const [text, setText] = useState('');
  const [counter, setCounter] = useState(0);
  const currentPolyCoordinates = polygon?.geometry?.coordinates;
  const currentFeature = map?.data.getFeatureById('test');

  const area =
    (currentPolyCoordinates && calculateArea(currentPolyCoordinates)) ||
    fieldDataV2?.boundary?.properties?.area;

  const setFeatureStyle = (
    feature: google.maps.Data.Feature,
  ): google.maps.Data.StyleOptions => {
    return {
      fillOpacity: feature.getProperty('fillOpacity') as number, // Set fill opacity
      fillColor: feature.getProperty('fillColor') as string, // Set fill color for the polygon
      strokeColor: feature.getProperty('color') as string, // Set stroke color based on the 'color' property
      strokeWeight: feature.getProperty('strokeWeight') as number,
      zIndex: feature.getProperty('zIndex') as number, // Set stroke weight as needed
      editable: true,
      clickable: true,
    };
  };

  const contentString = `<div
                            id="context-menu-delete-icon" style={{cursor: "pointer"}}
                          >Löschen</div`;

  const infowindow = new google.maps.InfoWindow({
    ariaLabel: 'Delete-Menu',
    content: contentString,
    disableAutoPan: true,
  });

  // This useEffect handles the initial drawing of the polygon on the map
  useEffect(() => {
    if (map) {
      // Add GeoJSON boundary data to the map
      fieldDataV2 && map.data.addGeoJson(fieldDataV2?.boundary);
      fieldDataV2 && setPolygon(fieldDataV2.boundary);
      // Set style for the added feature
      map.data.setStyle(setFeatureStyle);
    }

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [map, fieldDataV2]);

  const onEdit = (event: google.maps.PolyMouseEvent) => {
    const clickedPoint = event.latLng;
    const lngLatArray = clickedPoint && [
      clickedPoint.lng(),
      clickedPoint.lat(),
    ];

    // Check if the clicked point matches any polygon vertices and their lenght is greater then 3
    const isMatch =
      currentPolyCoordinates &&
      lngLatArray &&
      isPointInsideArrays(currentPolyCoordinates, lngLatArray);
    // Open the info window for deletion if there are enough points and we have a vertix match
    if (isMatch) {
      infowindow.open({ map });
      infowindow.setPosition(clickedPoint);
      infowindow.open({ map: map, shouldFocus: false });
      const coordinatesToWorkWith =
        lngLatArray &&
        currentPolyCoordinates &&
        removeFirstPointFromArray(lngLatArray, currentPolyCoordinates);
      // Remove the matched point and format the result for geometry modification
      const filteredAndConvertedCoordinates =
        coordinatesToWorkWith &&
        lngLatArray &&
        filterAndConvertCoordinates(coordinatesToWorkWith, lngLatArray);
      // Add a handler to the infowindow as soon as it gets domready
      google.maps.event.addListener(infowindow, 'domready', function () {
        const contextDeleteMenu = document.getElementById(
          'context-menu-delete-icon',
        );
        const infowindowDefaultCloseIcon =
          document.getElementsByClassName('gm-ui-hover-effect');
        if (infowindowDefaultCloseIcon instanceof HTMLCollection) {
          const infoWindowCloseX = infowindowDefaultCloseIcon[0] as HTMLElement;
          infoWindowCloseX.style.display = 'none';
        }
        if (contextDeleteMenu) {
          contextDeleteMenu.style.cursor = 'pointer';
          //Close the infowindow and set the new geometrie to the feature
          contextDeleteMenu.addEventListener('click', () => {
            infowindow.close();
            filteredAndConvertedCoordinates &&
              currentFeature?.setGeometry(
                new google.maps.Data.Polygon(filteredAndConvertedCoordinates),
              );
            currentFeature?.toGeoJson((data) => {
              setPolygon(data as GeoJsonFeaturePolygon);
              setText(JSON.stringify(data) + counter);
            });
          });
        }
      });
    }
  };

  const onSave = () => {
    currentFeature?.toGeoJson((data) => {
      setPolygon(data as GeoJsonFeaturePolygon);
      setText(JSON.stringify(data) + counter);
      setCounter(counter + 1);
    });
  };

  useEffect(() => {
    if (map) {
      // Close the info window anytime a user clicks anywhere on the map
      const mapClickHandler = google.maps.event.addListener(
        map,
        'click',
        () => {
          infowindow.close();
        },
      );

      // Handles click events on vertices
      const vertexClickHandler = map.data.addListener('click', onEdit);

      // Update the polygon to the state and remove the infowindow

      const saveGeometryHandler = map.data.addListener('mouseup', onSave);

      // Cleanup function removes all handlers to prevent multiple initiations on re-renders
      return () => {
        mapClickHandler.remove();
        vertexClickHandler.remove();
        saveGeometryHandler.remove();
      };
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [map, polygon]);
}

问题排查与修复

1. onSave函数状态更新延迟原因

  • 闭包旧值问题:onSave中使用的counter是组件渲染时的闭包值,React状态更新异步,setCounter(counter + 1)的新值要到下一次渲染才生效,导致setText无法同步显示最新状态,造成setPolygon未更新的错觉。
  • Stale Feature引用:currentFeature在组件顶层直接获取,不会随多边形修改自动更新,可能拿到旧实例。

修复后的onSave函数

const onSave = () => {
  // 实时获取最新的Feature实例
  const latestFeature = map?.data.getFeatureById('test');
  latestFeature?.toGeoJson((data) => {
    setPolygon(data as GeoJsonFeaturePolygon);
    // 使用函数式更新获取最新counter值
    setCounter(prevCounter => {
      const newCounter = prevCounter + 1;
      setText(JSON.stringify(data) + newCounter);
      return newCounter;
    });
  });
};

整体代码优化建议

1. 避免Stale状态/引用

  • 不在组件顶层缓存currentFeature,而是在onEdit、onSave等函数内部实时获取,确保拿到最新实例。
  • 依赖前一次状态更新时,使用函数式更新(如setCounter(prev => prev + 1)),规避闭包旧值问题。

2. 优化事件监听器管理

  • 将地图事件监听器的绑定放在仅依赖map的useEffect中,避免因polygon变化频繁解绑/重绑。
  • infowindow的domready监听器会重复绑定,建议在添加前移除旧监听器,或放在useEffect中统一管理并清理。

3. 优化初始渲染逻辑

  • 初始添加GeoJSON时,先检查是否已存在ID为test的Feature,避免重复添加:
useEffect(() => {
  if (map && fieldDataV2) {
    const existingFeature = map.data.getFeatureById('test');
    if (!existingFeature) {
      map.data.addGeoJson(fieldDataV2.boundary);
    }
    setPolygon(fieldDataV2.boundary);
    map.data.setStyle(setFeatureStyle);
  }
}, [map, fieldDataV2]);

4. 类型安全与可读性提升

  • contentString中的style是字符串模板,改用普通CSS语法:style="cursor: pointer"。
  • 给isPointInsideArrays、removeFirstPointFromArray等工具函数添加类型定义。
  • 将infowindow的创建放在useEffect中,避免每次渲染重复创建:
const [infowindow, setInfowindow] = useState<google.maps.InfoWindow | null>(null);

useEffect(() => {
  if (map) {
    const infoWindowInstance = new google.maps.InfoWindow({
      ariaLabel: 'Delete-Menu',
      content: `<div id="context-menu-delete-icon" style="cursor: pointer">Löschen</div>`,
      disableAutoPan: true,
    });
    setInfowindow(infoWindowInstance);
    
    return () => {
      infoWindowInstance.close();
    };
  }
}, [map]);

5. 减少不必要渲染

  • 用useMemo缓存currentPolyCoordinates,避免每次渲染重复计算:
const currentPolyCoordinates = useMemo(() => polygon?.geometry?.coordinates, [polygon]);
  • 若text仅用于调试,生产环境可直接移除,减少状态更新触发的渲染。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 22:57:03