React Google Maps API MarkerClusterer 重新渲染

问题描述 投票:0回答:1

我正在开发一个近距离应用程序。数据库包含每个项目的经度、纬度和 geohash。客户端向服务器提供 GPS 坐标。然后,服务器将这些坐标转换为 geohash,并返回这些坐标附近的所有 GPS 点。实际上它看起来像这样:

enter image description here

概念验证工作顺利,但我在标记集群器实施方面遇到了困难。这是我目前得到的:

import { AdvancedMarker, Map, MapEvent } from '@vis.gl/react-google-maps';

我正在使用

"@vis.gl/react-google-maps": "^0.11.2",

此函数获取并设置数据:

async function fetchGlobalData(lng: number, lat: number) {
    try {
        const response = await axios.get(`${apiURL}/api/location-data?posX=${lng}&posY=${lat}`);
        setItemsLocationData(response.data);
    } catch (error: any) {
        console.error(error);
    }
}

谷歌地图组件:

<Map
    mapId={mapId}
    style={containerStyle}
    defaultCenter={{ lat: gpsLocation.lat, lng: gpsLocation.lng }}
    zoomControl={true}
    defaultZoom={defaultMapZoom}
    minZoom={minZoom}
    gestureHandling={'greedy'}
    disableDefaultUI={true}
    onIdle={(e: MapEvent<unknown>) => {
        let lng = e.map.getCenter()?.lng()
        let lat = e.map.getCenter()?.lat()
        let tempGeoHash = lng && lat && geohash.encode(lat, lng, 3)
        if (currentGeoHash !== tempGeoHash && lat && lng) {
            fetchGlobalData(lng, lat)
        }
        tempGeoHash && setCurrentGeoHash(tempGeoHash)
    }}
>
    <Markers coords={trainingSpotsLocationData} />
</Map>

这是我的标记组件,它呈现所有标记:

const Markers = ({ coords }: ImarkerProps) => {
    return (
        <>
            {coords.map(x => {
                return (
                    <AdvancedMarker
                        position={{ lng: x.posX, lat: x.posY }}
                        key={x.id}
                    ></AdvancedMarker>
                )
            })}
        </>
    )
}

上面的代码运行没有问题。当我尝试实现 Marker Clusterer 时,问题就开始了。

我尝试使用 Leigh Halliday 的示例以及提供的源代码。我尝试按照示例中指定的方式从

MarkerClusterer
库实现
@googlemaps/markerclusterer

已更新

下面是一些硬编码数据的重现错误。如果您只需使用您自己的 Google 地图 API 密钥和地图 ID 将其粘贴到您的 index.tsx 中,它就应该可以工作。

我尝试遵循这个例子。不确定这个实现是否适合标记数据可以动态的场景。

使用的封装:

"@googlemaps/markerclusterer": "^2.5.3",
"@vis.gl/react-google-maps": "^1.0.0",
import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom/client";
import {
  AdvancedMarker,
  Map,
  useMap,
  APIProvider,
} from "@vis.gl/react-google-maps";
import { Marker, MarkerClusterer } from "@googlemaps/markerclusterer";

const rootElement = document.getElementById("root")!;
const root = ReactDOM.createRoot(rootElement);

const App = () => {
  const [trainingSpotsLocationData, setTrainingSpotsLocationData] = useState<
    IMapMarkers[]
  >([]);

  const dataset1 = [
    { id: "4a49d262", posX: 19.03565563713926, posY: 47.50797104332087 },
    { id: "3a432f12", posX: 19.03165563793341, posY: 47.50797104332977 },
  ];
  const dataset2 = [
    { id: "28546515", posX: 18.913425585937492, posY: 47.51137228850743 },
    { id: "411bb9e5", posX: 18.675846240234367, posY: 47.46621059365244 },
    { id: "f832f6f1", posX: 18.579715869140617, posY: 47.478278133922885 },
  ];

  // dataset3 is a superset of of dataset2. We can switch between dataset1 and dataset2 
  // without any issues, but becasue dataset3 contains items that were present in dataset1
  // or dataset2 the clusterer breaks
  const dataset3 = [
    { id: "4a49d262", posX: 19.03565563713926, posY: 47.50797104332087 },
    { id: "3a432f12", posX: 19.03165563793341, posY: 47.50797104332977 },
    { id: "28546515", posX: 18.913425585937492, posY: 47.51137228850743 },
    { id: "411bb9e5", posX: 18.675846240234367, posY: 47.46621059365244 },
    { id: "f832f6f1", posX: 18.579715869140617, posY: 47.478278133922885 },
  ];

  return (
    <>
      <button
        onClick={() => {
          setTrainingSpotsLocationData(dataset1);
        }}
      >
        Load dataset1
      </button>
      <button
        onClick={() => {
          setTrainingSpotsLocationData(dataset2);
        }}
      >
        Load dataset2
      </button>
      <button
        onClick={() => {
          setTrainingSpotsLocationData(dataset3);
        }}
      >
        Load dataset3
      </button>
      <APIProvider apiKey={"Your API key"}>
        <Map
          mapId={"Your map ID"}
          style={{ width: "500px", height: "500px" }}
          defaultCenter={{ lng: 19.0947, lat: 47.5636 }}
          zoomControl={true}
          defaultZoom={8}
          minZoom={1}
          disableDefaultUI={true}
        >
          <Markers coords={trainingSpotsLocationData} />
        </Map>
      </APIProvider>
    </>
  );
};

export interface IMapMarkers {
  id: string;
  posX: number;
  posY: number;
}

export interface ImarkerProps {
  coords: IMapMarkers[];
}

const Markers = ({ coords }: ImarkerProps) => {
  const map = useMap();
  const [markers, setMarkers] = useState<{ [key: string]: any }>({});
  const clusterer = useRef<MarkerClusterer | null>(null);

  useEffect(() => {
    console.log(markers);
    clusterer.current?.clearMarkers();
    clusterer.current?.addMarkers(Object.values(markers));
  }, [markers]);

  useEffect(() => {
    if (!map) return;
    if (!clusterer.current) {
      clusterer.current = new MarkerClusterer({ map });
    }
  }, [map]);

  const setMarkerRef = (marker: Marker | null, key: string) => {
    if (marker && markers[key]) return;
    if (!marker && !markers[key]) return;

    setMarkers((prev) => {
      if (marker) {
        return { ...prev, [key]: marker };
      } else if (!marker) {
        const newMarkers = { ...prev };
        delete newMarkers[key];
        return newMarkers;
      }
    });
  };

  return (
    <>
      {coords.map((x) => {
        return (
          <AdvancedMarker
            position={{ lng: x.posX, lat: x.posY }}
            key={x.id}
            ref={(marker) => setMarkerRef(marker, x.id)}
          ></AdvancedMarker>
        );
      })}
    </>
  );
};

root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

我的 tsconfig.json:

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": [
    "src"
  ]
}

如果我从空数据集转换到数据集 3,它似乎确实有效,但是如果我从数据集 1 转换到数据集 3,我会得到

ReactJS: Maximum update depth exceeded error
。仅当新返回的数据包含状态数据中已有的一些记录时,才会发生这种情况。

我的 API 正在基于 geohash + 8 个最近邻居查询标记,因此如果下一个查询来自相邻的 geohash,则结果数据将包含上一个查询中的一些标记。因为 API 是无状态的,所以它不会跟踪以前加载的标记,因此我想在客户端处理它

我不确定为什么会发生这种情况,因为我的理解是这个 useEffect 在设置新标记之前会清除它;

  useEffect(() => {
    clusterer.current?.clearMarkers();
    clusterer.current?.addMarkers(Object.values(markers));
  }, [markers]);

如上所述,只有当新数据集至少包含先前加载的数据集中已存在的一条记录时,才会发生这种情况。

我的项目示例:

如果我进一步拖动地图以确保新旧数据之间没有重复,则它可以正常工作: enter image description here

任何人都可以提供一个使用 Marker Clusterer 处理此场景的示例片段,其中地图数据可以更改,并且某些更改的数据可能是相同的?任何关于为什么会发生这种情况的见解将不胜感激!

我尝试从

MarkerClusterer
库实现
@googlemaps/markerclusterer
,如上面示例中指定的那样,我希望它的工作方式与静态数据相同,但事实并非如此。

我正在寻找一种在数据加载之间尽可能少重新渲染的解决方案。

reactjs google-maps-api-3 google-maps-markers react-google-maps react-google-maps-api
1个回答
0
投票

我能够使用这个解决方案解决它

import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom/client";
import {
  AdvancedMarker,
  Map,
  useMap,
  APIProvider,
  useAdvancedMarkerRef,
} from "@vis.gl/react-google-maps";
import { Marker, MarkerClusterer } from "@googlemaps/markerclusterer";

const rootElement = document.getElementById("root")!;
const root = ReactDOM.createRoot(rootElement);

const App = () => {
  const [trainingSpotsLocationData, setTrainingSpotsLocationData] = useState<
    IMapMarkers[]
  >([]);

  const dataset1 = [
    { id: "4a49d262", posX: 19.03565563713926, posY: 47.50797104332087 },
    { id: "3a432f12", posX: 19.03165563793341, posY: 47.50797104332977 },
  ];
  const dataset2 = [
    { id: "28546515", posX: 18.913425585937492, posY: 47.51137228850743 },
    { id: "411bb9e5", posX: 18.675846240234367, posY: 47.46621059365244 },
    { id: "f832f6f1", posX: 18.579715869140617, posY: 47.478278133922885 },
  ];

  const dataset3 = [
    { id: "4a49d262", posX: 19.03565563713926, posY: 47.50797104332087 },
    { id: "3a432f12", posX: 19.03165563793341, posY: 47.50797104332977 },
    { id: "28546515", posX: 18.913425585937492, posY: 47.51137228850743 },
    { id: "411bb9e5", posX: 18.675846240234367, posY: 47.46621059365244 },
    { id: "f832f6f1", posX: 18.579715869140617, posY: 47.478278133922885 },
  ];

  return (
    <>
      <button
        onClick={() => {
          setTrainingSpotsLocationData(dataset1);
        }}
      >
        Load dataset1
      </button>
      <button
        onClick={() => {
          setTrainingSpotsLocationData(dataset2);
        }}
      >
        Load dataset2
      </button>
      <button
        onClick={() => {
          setTrainingSpotsLocationData(dataset3);
        }}
      >
        Load dataset3
      </button>
      <APIProvider apiKey={"your API kez"}>
        <Map
          mapId={"your map id"}
          style={{ width: "500px", height: "500px" }}
          defaultCenter={{ lng: 19.0947, lat: 47.5636 }}
          zoomControl={true}
          defaultZoom={8}
          minZoom={1}
          disableDefaultUI={true}
        >
          <CustomMarkers locations={trainingSpotsLocationData} />
        </Map>
      </APIProvider>
    </>
  );
};

export interface IMapMarkers {
  id: string;
  posX: number;
  posY: number;
}

type CustomMarkerProps = {
  location: IMapMarkers;
  clusterer: MarkerClusterer | null;
};

const CustomMarker = ({ location, clusterer }: CustomMarkerProps) => {
  // Marker Ref
  const [refCallback, marker] = useAdvancedMarkerRef();

  useEffect(() => {
    if (!clusterer || !marker) return;
    clusterer.addMarker(marker);

    return () => {
      clusterer.removeMarker(marker);
    };
  }, [clusterer, marker]);

  return (
    <AdvancedMarker
      position={{ lng: location.posX, lat: location.posY }}
      key={location.id}
      ref={refCallback}
    />
  );
};

const CustomMarkers = ({ locations }: { locations: IMapMarkers[] }) => {
  const map = useMap();
  const clusterer = useRef<MarkerClusterer | null>(null);

  useEffect(() => {
    if (!map) return;
    if (!clusterer.current) {
      clusterer.current = new MarkerClusterer({ map });
    }
  }, [map]);

  return (
    <>
      {locations.map((location) => (
        <CustomMarker
          key={location.id}
          location={location}
          clusterer={clusterer.current}
        />
      ))}
    </>
  );
};

root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

© www.soinside.com 2019 - 2024. All rights reserved.