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

React18 + @react-google-maps/api路线残留问题求助

解决@react-google-maps/api切换路线后旧路线残留问题

问题描述

使用Next.js 12.3.1和React.js 18.2.0开发项目,集成@react-google-maps/api实现地图路线功能。选择起点A和终点B时,DirectionsService与DirectionsRenderer可正常渲染路线及标记;但切换起点或终点后,新路线能正常显示,最初的路线/线条却残留在地图上。

问题原因

  1. 旧路线状态未清除:起点、终点或途经点变化时,directions状态未重置为null,导致DirectionsRenderer同时渲染新旧路线。
  2. 途经点格式错误:生成waypoints时,将location对象转成字符串,不符合Google Maps API要求的格式,可能引发路线计算异常或重复渲染。
  3. 组件更新逻辑不严谨:useCallback和useEffect的依赖数组不完整,加上call状态触发时机问题,导致路线请求与状态更新不同步。
  4. Marker的Key设置不合理:仅用坐标值作为Key,无法精准触发组件重新渲染。

解决方案

步骤1:重置路线状态

在起点、终点或途经点变化时,立即清空旧的路线数据,确保新路线渲染前旧路线被移除。

步骤2:修复途经点格式

移除location对象的toString()转换,直接传入坐标对象。

步骤3:完善组件依赖项

补全useCallback和useEffect的依赖数组,避免因依赖缺失导致的状态更新不及时。

步骤4:优化Marker的Key值

使用包含坐标和类型的复合Key,确保位置变化时Marker能正确重新渲染。

修改后的代码

CustomGoogleMap组件

import React, { useEffect, useState, memo } from "react";
import {
  GoogleMap,
  MarkerF,
  DirectionsRenderer,
  DirectionsService,
  InfoWindow,
  LoadScriptNext,
} from "@react-google-maps/api";
import LoadingMarker from "../SvgIcons/loading-marker.svg";
import UnLoadingMarker from "../SvgIcons/unloading-marker.svg";
import StoppageIcon from "../SvgIcons/stopageIcon.svg";
import { isEmptyArray } from "@services/utils";
import { anyObjectType, Dispatcher } from "@services/types";

const containerStyle = {
  width: "100%",
  height: "100%",
};

const center = {
  lat: 23.810331,
  lng: 90.412521,
};

interface CustomLatLong {
  latitude: number;
  longitude: number;
  textEn: string;
}

type CustomDirectionsWaypoint = google.maps.DirectionsWaypoint & CustomLatLong;

interface CustomGoogleMapProps {
  call: boolean;
  setCall: Dispatcher<boolean>;
  handleStopageDragLocation?: (a: number, b: number, c: number) => void;
  handleLoadingPointDragLocation?: (a: number, b: number) => void;
  handleUnLoadingPointDragLocation?: (a: number, b: number) => void;
  waypointsList2: CustomDirectionsWaypoint[];
  loadingP: anyObjectType;
  unloadingP: anyObjectType;
  isAddressModal?: boolean;
}

const CustomGoogleMap: React.FC<CustomGoogleMapProps> = (props) => {
  const {
    call,
    setCall,
    loadingP,
    unloadingP,
    waypointsList2,
    handleLoadingPointDragLocation,
    handleUnLoadingPointDragLocation,
    handleStopageDragLocation,
    isAddressModal = false,
  } = props;

  const [infoBox, setInfoWindow] = useState({
    position: {
      lat: 0,
      lng: 0,
    },
    info: "",
  });

  const [directions, setDirections] = useState<google.maps.DirectionsResult | null>(null);

  // 路线参数变化时重置状态并触发请求
  useEffect(() => {
    if (loadingP.latitude && unloadingP.latitude) {
      setDirections(null);
      setCall(true);
    }
  }, [loadingP.latitude, unloadingP.latitude, waypointsList2, setCall]);

  const directionsCallback = (response: google.maps.DirectionsResult | null) => {
    if (response !== null) {
      if (response.status === "OK") {
        setCall(false);
        setDirections(response);
      }
    }
  };

  const getDirectionRender = () => {
    return (
      <DirectionsService
        options={{
          destination: {
            lat: unloadingP.latitude,
            lng: unloadingP.longitude,
          },
          origin: {
            lat: loadingP.latitude,
            lng: loadingP.longitude,
          },
          travelMode: google.maps.TravelMode.DRIVING,
          waypoints: waypointsList2.map(({ latitude, longitude }) => ({
            location: { lat: latitude, lng: longitude },
            stopover: false,
          })),
        }}
        callback={directionsCallback}
      />
    );
  };

  const renderMap = () => {
    return (
      <LoadScriptNext
        googleMapsApiKey={process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!}
      >
        <GoogleMap
          mapContainerStyle={containerStyle}
          center={
            loadingP.latitude
              ? {
                  lat: loadingP.latitude,
                  lng: loadingP.longitude,
                }
              : center
          }
          zoom={10}
          onClick={({ latLng }) => {
            isAddressModal &&
              handleLoadingPointDragLocation &&
              handleLoadingPointDragLocation(latLng!.lat(), latLng!.lng());
          }}
        >
          {loadingP.latitude && unloadingP.latitude && call && getDirectionRender()}
          {directions && (
            <DirectionsRenderer
              options={{
                directions: directions,
                suppressMarkers: true,
              }}
            />
          )}

          {infoBox.position.lat !== 0 && (
            <InfoWindow
              position={infoBox.position}
              onCloseClick={() =>
                setInfoWindow({
                  position: { lat: 0, lng: 0 },
                  info: "",
                })
              }
            >
              <p>{infoBox.info}</p>
            </InfoWindow>
          )}

          <MarkerF
            key={`loading-${loadingP.latitude}-${loadingP.longitude}`}
            draggable
            onDragEnd={({ latLng }) => {
              handleLoadingPointDragLocation &&
                handleLoadingPointDragLocation(latLng!.lat(), latLng!.lng());
            }}
            position={{ lat: loadingP.latitude, lng: loadingP.longitude }}
            icon={LoadingMarker}
            onClick={() =>
              setInfoWindow({
                ...infoBox,
                position: { lat: loadingP.latitude, lng: loadingP.longitude },
                info: loadingP.textEn,
              })
            }
          />
          <MarkerF
            key={`unloading-${unloadingP.latitude}-${unloadingP.longitude}`}
            draggable
            onDragEnd={({ latLng }) => {
              handleUnLoadingPointDragLocation &&
                handleUnLoadingPointDragLocation(latLng!.lat(), latLng!.lng());
            }}
            position={{ lat: unloadingP.latitude, lng: unloadingP.longitude }}
            icon={UnLoadingMarker}
            onClick={() =>
              setInfoWindow({
                ...infoBox,
                position: { lat: unloadingP.latitude, lng: unloadingP.longitude },
                info: unloadingP.textEn,
              })
            }
          />
          {!isEmptyArray(waypointsList2) &&
            waypointsList2.map((item, index) => (
              <MarkerF
                key={`waypoint-${index}-${item.latitude}-${item.longitude}`}
                draggable
                visible
                onDragEnd={({ latLng }) => {
                  handleStopageDragLocation &&
                    handleStopageDragLocation(latLng!.lat(), latLng!.lng(), index);
                }}
                position={{ lat: item.latitude, lng: item.longitude }}
                icon={StoppageIcon}
                onClick={() =>
                  setInfoWindow({
                    ...infoBox,
                    position: { lat: item.latitude, lng: item.longitude },
                    info: item.textEn,
                  })
                }
              />
            ))}
        </GoogleMap>
      </LoadScriptNext>
    );
  };
  return renderMap();
};

export default memo(CustomGoogleMap);

BookingMap组件

import React, { useCallback, useState } from "react";
import CustomGoogleMap from "@components/core/CustomGoogleMap";
import { Box } from "@mui/material";
import { anyObjectType } from "@services/types";
import MapAPI from "@services/api/map";
import { CreateBookingActions } from "@services/constants";
import { useCreateBookingContext } from "@contexts/CreateBookingContext";

const BookingMap: React.FC<BookingMapProps> = ({
  memoizedloadingPoint,
  memoizedunloadingPoint,
  memoizedwaypointsList,
  setToast,
  setLoadingPoint,
  setUnLoadingPoint,
}) => {
  const cbCtx = useCreateBookingContext();
  const { state, dispatch } = cbCtx;
  const { waypointsList } = state;
  const [call, setCall] = useState(false);

  const handleLoadingPointDragLocation = useCallback(
    async (lat: number, lng: number) => {
      if (lat && lng) {
        const query = { latlng: `${lat},${lng}` };
        const { response, error }: any = await MapAPI.onDrag(query);
        if (response) {
          setLoadingPoint({ ...response.data.data });
        } else {
          setToast("Something went wrong", "error");
        }
      }
    },
    [setLoadingPoint, setToast]
  );

  const handleUnLoadingPointDragLocation = useCallback(
    async (lat: number, lng: number) => {
      if (lat && lng) {
        const query = { latlng: `${lat},${lng}` };
        const { response, error }: any = await MapAPI.onDrag(query);
        if (response) {
          setUnLoadingPoint({ ...response.data.data });
        } else {
          setToast("Something went wrong", "error");
        }
      }
    },
    [setUnLoadingPoint, setToast]
  );

  const setWaypointsList = useCallback((val: any[]) => {
    dispatch({
      type: CreateBookingActions.SET_WAYPOINT_LIST,
      payload: val,
    });
  }, [dispatch]);

  const handleStopageDragLocation = useCallback(
    async (lat: number, lng: number, index: number) => {
      if (lat && lng) {
        const query = { latlng: `${lat},${lng}` };
        const { response, error }: any = await MapAPI.onDrag(query);
        if (response) {
          const updatedWaypoints = [...waypointsList];
          updatedWaypoints[index] = { ...response.data.data };
          setWaypointsList(updatedWaypoints);
          setCall(true);
        } else {
          setToast("Something went wrong", "error");
        }
      }
    },
    [waypointsList, setWaypointsList, setToast]
  );

  return (
    <Box sx={{ height: "420px" }}>
      <CustomGoogleMap
        call={call}
        setCall={setCall}
        loadingP={memoizedloadingPoint}
        unloadingP={memoizedunloadingPoint}
        waypointsList2={memoizedwaypointsList}
        handleLoadingPointDragLocation={handleLoadingPointDragLocation}
        handleUnLoadingPointDragLocation={handleUnLoadingPointDragLocation}
        handleStopageDragLocation={handleStopageDragLocation}
      />
    </Box>
  );
};

export default BookingMap;

interface BookingMapProps {
  memoizedloadingPoint: anyObjectType;
  memoizedunloadingPoint: anyObjectType;
  memoizedwaypointsList: any[];
  setToast: (a: string, b: string) => void;
  setLoadingPoint: (a: anyObjectType) => void;
  setUnLoadingPoint: (a: anyObjectType) => void;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 00:35:24