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

React StrictMode下Google Maps地理围栏绘制功能失效求助

问题

我有一个React v18.2(TypeScript)应用,需要为用户展示Google地图并提供地理围栏(geofence)功能,允许用户在地图上绘制多边形。使用@react-google-maps/api v2.18.1集成地图时,不开启React.StrictMode一切正常,但开启后地理围栏UI无法渲染,用户无法使用绘制功能。

请问该问题与React StrictMode有何关联?是否有解决方法能让我在开启React.StrictMode的同时正常使用地理围栏功能?

以下是原组件代码:

Map组件

import { Polygon, GoogleMap, useLoadScript } from '@react-google-maps/api';
import { DrawingManager } from './drawing-manager';

interface MapsProps {
  lat: number;
  lng: number;
  isStatic: boolean;
  zoom?: number;
  isDrawable?: boolean;
}

interface Shape {
  type: string;
  path: google.maps.LatLngLiteral[];
  radius?: number;
}

const { useRef, useMemo, useState, useCallback } = React;
const __mapMandatoryStyles = { width: '100%', height: '100%' }; // ⌂ Keep it like this and style the parent if needed!
const googleMapsApiKey = process.env.REACT_APP_GOOGLE_MAPS_API_KEY as string;
const libraries: ('drawing' | 'geometry' | 'localContext' | 'places' | 'visualization')[] = [
  'drawing',
]; // ⌂ Do NOT touch this type! It comes from the '@react-google-maps/api' lib but it's not exported from there.

export const Map = ({
  lat,
  lng,
  isStatic,
  zoom = 15,
  isDrawable = false,
}: MapsProps): JSX.Element | null => {
  const [shapes, setShapes] = useState<Array<Shape>>([]);

  const { isLoaded } = useLoadScript({
    id: 'google-map-script',
    googleMapsApiKey,
    libraries,
  });

  const mapRef = useRef<google.maps.Map | null>(null);
  const drawingManagerRef = useRef(null);

  const handleMapLoad = useCallback((map: google.maps.Map) => {
    if (mapRef?.current) {
      mapRef.current = map;
    }
  }, []);

  const handleDraw = useCallback(
    (shape: Shape) => {
      setShapes([...shapes, shape]);
    },
    [shapes]
  );

  const handleOnPolygonComplete = useCallback(
    (polygon: google.maps.Polygon) => {
      if (polygon.getPath() !== null) {
        const polygonPathArray = polygon.getPath().getArray();

        if (polygonPathArray) {
          const path = polygonPathArray.map(latLngLiteral => {
            return { lat: latLngLiteral.lat(), lng: latLngLiteral.lng() };
          });

          handleDraw({ path, type: 'polygon' });
        }
      }
    },
    [handleDraw]
  );

  const renderShapes = useCallback(
    () =>
      shapes.map((shape, idx): JSX.Element | null =>
        shape.type === 'polygon' ? (
          <Polygon key={idx} path={shape.path.map(latLng => latLng)} />
        ) : null
      ),
    [shapes]
  );

  const getMapOptions = useMemo(
    () => ({
      zoomControl: !isStatic,
      scrollwheel: !isStatic,
      rotateControl: !isStatic,
      clickableIcons: !isStatic,
      mapTypeControl: !isStatic,
      keyboardShortcuts: !isStatic,
      fullscreenControl: !isStatic,
      streetViewControl: !isStatic,
      disableDoubleClickZoom: isStatic,
      isFractionalZoomEnabled: !isStatic,
      gestureHandling: isStatic ? 'none' : 'auto',
    }),
    [isStatic]
  );

  if (isDrawable) {
    return isLoaded ? (
      <GoogleMap
        zoom={zoom}
        center={{ lat, lng }}
        options={getMapOptions}
        mapContainerStyle={__mapMandatoryStyles}
        onLoad={handleMapLoad}
      >
        <DrawingManager
          ref={drawingManagerRef}
          options={{
            drawingControl: true,
            drawingControlOptions: {
              position: google.maps.ControlPosition.TOP_CENTER,
              drawingModes: [google?.maps?.drawing?.OverlayType.POLYGON],
            },
          }}
          onPolygonComplete={handleOnPolygonComplete}
        />

        {renderShapes()}
      </GoogleMap>
    ) : null;
  }

  return isLoaded ? (
    <GoogleMap
      zoom={zoom}
      center={{ lat, lng }}
      options={getMapOptions}
      mapContainerStyle={__mapMandatoryStyles}
    />
  ) : null;
};

DrawingManager组件

import * as React from 'react';
import {
  DrawingManager as RactGoogleMapsDrawingManager,
  DrawingManagerProps,
} from '@react-google-maps/api';

export const DrawingManager = React.forwardRef<RactGoogleMapsDrawingManager, DrawingManagerProps>(
  (props, ref): JSX.Element => <RactGoogleMapsDrawingManager ref={ref} {...props} />
);

DrawingManager.displayName = 'DrawingManager';
原因分析

React StrictMode在React 18中会双重执行组件初始化逻辑、副作用(useEffect)及回调函数,目的是检测未清理的副作用、非幂等初始化等潜在bug。

你的问题核心在于:

  1. @react-google-maps/api的DrawingManager在StrictMode的双重渲染下出现实例冲突:第一次渲染创建的实例会被第二次校验渲染销毁,最终导致绘制控件未正确绑定到地图。
  2. handleDraw依赖shapes状态,StrictMode下重复执行会导致回调引用不稳定,影响绘制完成后的状态更新。
解决方法

针对StrictMode特性,通过以下调整修复问题:

1. 用函数式更新修复handleDraw的 stale state 问题

移除shapes依赖,直接通过函数式更新获取最新状态:

const handleDraw = useCallback(
  (shape: Shape) => {
    setShapes(prevShapes => [...prevShapes, shape]);
  },
  [] // 不再依赖shapes
);

2. 优化DrawingManager实例管理

添加useEffect确保实例只正确初始化一次,组件卸载时清理实例:

useEffect(() => {
  const drawingManager = drawingManagerRef.current;
  if (mapRef.current && drawingManager && isDrawable) {
    drawingManager.setMap(mapRef.current);
  }
  return () => {
    if (drawingManager) {
      drawingManager.setMap(null);
    }
  };
}, [isDrawable]);

3. 修正handleMapLoad逻辑

直接赋值map实例,无需多余判断:

const handleMapLoad = useCallback((map: google.maps.Map) => {
  mapRef.current = map;
}, []);

4. 简化renderShapes(可选)

去掉不必要的useCallback,简化渲染逻辑:

const renderShapes = () =>
  shapes.map((shape, idx) =>
    shape.type === 'polygon' ? (
      <Polygon key={idx} path={shape.path} />
    ) : null
  );
修改后的完整Map组件
import { Polygon, GoogleMap, useLoadScript } from '@react-google-maps/api';
import { DrawingManager } from './drawing-manager';

interface MapsProps {
  lat: number;
  lng: number;
  isStatic: boolean;
  zoom?: number;
  isDrawable?: boolean;
}

interface Shape {
  type: string;
  path: google.maps.LatLngLiteral[];
  radius?: number;
}

const { useRef, useMemo, useState, useCallback, useEffect } = React;
const __mapMandatoryStyles = { width: '100%', height: '100%' };
const googleMapsApiKey = process.env.REACT_APP_GOOGLE_MAPS_API_KEY as string;
const libraries: ('drawing' | 'geometry' | 'localContext' | 'places' | 'visualization')[] = ['drawing'];

export const Map = ({
  lat,
  lng,
  isStatic,
  zoom = 15,
  isDrawable = false,
}: MapsProps): JSX.Element | null => {
  const [shapes, setShapes] = useState<Array<Shape>>([]);

  const { isLoaded } = useLoadScript({
    id: 'google-map-script',
    googleMapsApiKey,
    libraries,
  });

  const mapRef = useRef<google.maps.Map | null>(null);
  const drawingManagerRef = useRef<google.maps.drawing.DrawingManager | null>(null);

  const handleMapLoad = useCallback((map: google.maps.Map) => {
    mapRef.current = map;
  }, []);

  const handleDraw = useCallback(
    (shape: Shape) => {
      setShapes(prevShapes => [...prevShapes, shape]);
    },
    []
  );

  const handleOnPolygonComplete = useCallback(
    (polygon: google.maps.Polygon) => {
      const path = polygon.getPath()?.getArray().map(latLng => ({
        lat: latLng.lat(),
        lng: latLng.lng(),
      }));
      if (path) {
        handleDraw({ path, type: 'polygon' });
      }
      // 可选:移除绘制完成后临时显示的多边形
      polygon.setMap(null);
    },
    [handleDraw]
  );

  const renderShapes = () =>
    shapes.map((shape, idx) =>
      shape.type === 'polygon' ? (
        <Polygon key={idx} path={shape.path} />
      ) : null
    );

  const getMapOptions = useMemo(
    () => ({
      zoomControl: !isStatic,
      scrollwheel: !isStatic,
      rotateControl: !isStatic,
      clickableIcons: !isStatic,
      mapTypeControl: !isStatic,
      keyboardShortcuts: !isStatic,
      fullscreenControl: !isStatic,
      streetViewControl: !isStatic,
      disableDoubleClickZoom: isStatic,
      isFractionalZoomEnabled: !isStatic,
      gestureHandling: isStatic ? 'none' : 'auto',
    }),
    [isStatic]
  );

  useEffect(() => {
    const drawingManager = drawingManagerRef.current;
    if (mapRef.current && drawingManager && isDrawable) {
      drawingManager.setMap(mapRef.current);
    }
    return () => {
      if (drawingManager) {
        drawingManager.setMap(null);
      }
    };
  }, [isDrawable]);

  if (isDrawable) {
    return isLoaded ? (
      <GoogleMap
        zoom={zoom}
        center={{ lat, lng }}
        options={getMapOptions}
        mapContainerStyle={__mapMandatoryStyles}
        onLoad={handleMapLoad}
      >
        <DrawingManager
          ref={drawingManagerRef}
          options={{
            drawingControl: true,
            drawingControlOptions: {
              position: google.maps.ControlPosition.TOP_CENTER,
              drawingModes: [google.maps.drawing.OverlayType.POLYGON],
            },
          }}
          onPolygonComplete={handleOnPolygonComplete}
        />
        {renderShapes()}
      </GoogleMap>
    ) : null;
  }

  return isLoaded ? (
    <GoogleMap
      zoom={zoom}
      center={{ lat, lng }}
      options={getMapOptions}
      mapContainerStyle={__mapMandatoryStyles}
    />
  ) : null;
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 22:03:12