Sebastian Meckovski
Sebastian Meckovski

Reputation: 278

React Google Maps API MarkerClusterer rerender

I'm working on a proximity app. The database contains longitude, latitude and a geohash of each item. The client supplies GPS coordinates to the server. The server then converts those coordinates to a geohash, and returns all gps points in the vicinity of those coordinates. In practice it looks like this:

enter image description here

The proof of concept works smoothly but I'm struggling with Marker Clusterer implementation. Here's what I currently got:

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

I'm using "@vis.gl/react-google-maps": "^0.11.2",

This function fetches and sets the data:

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

Google maps component:

<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>

This is my Markers component that renders all the markers:

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

Code above works as without issues. Issues start when I try to implement Marker Clusterer.

I tried going with Leigh Halliday's example with the supplied source code. I tried implementing the MarkerClusterer from the@googlemaps/markerclusterer library as specified in the example.

UPDATED:

Below is the reproduced error with some hard-coded data. It should work if you simply paste it to your index.tsx, with your own Google Maps API key and map Id.

I tried to follow this example. Not sure if this implementation is suitable for scenarios where the marker data can by dynamic.

packages used:

"@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>
);

My 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"
  ]
}

It does seem to be working if I transition from empty dataset to dataset3, however if I'm transitioning from dataset 1 to 3, I'm getting ReactJS: Maximum update depth exceeded error. This only happens if the new returned data contains some records that were already in the state data.

My API is querying markers based on a geohash + 8 nearest neighbours, so if the next query is from a neighbouring geohash, the resulting data will contain some markers that were in the previous query. And because the API is stateless, it's not tracking previously loaded markers, therefore I'd like to handle it on the client side

I'm not sure why it's happening as my understaning is that this useEffect clears it before setting new markers;

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

As mentioned, this only happens if the new dataset contains at least one record that was already present in the previously loaded dataset.

Example from my project:

If I drag the map further to ensure there are no duplicates between new and old data, it then works fine: enter image description here

Can anyone suppply an example snippet that handles this scenario with Marker Clusterer, where map data can change and some of that changed data might be the same? Any insights on why this might be happening would be gladly appreciated!

I tried implementing the MarkerClusterer from the@googlemaps/markerclusterer library as specified in the example above, I was expecting this to work the same way as with static data, but that was not the case.

I'm looking for a solution with as little rerenders as possible between data loads.

Upvotes: 4

Views: 668

Answers (2)

Batja
Batja

Reputation: 89

  1. Need to render Markers component after data is fetched and not rendering while is fetching.
  2. clear markers (clusterer.current?.clearMarkers()) when Markers component unmounted (useEffect's return)

Upvotes: 1

Sebastian Meckovski
Sebastian Meckovski

Reputation: 278

Looks like visgl has released a better example that handles dynamic markers

https://github.com/visgl/react-google-maps/commit/2bea503d7fb9f7f67b6e1d0d92146a8f8940ec5b

Upvotes: 1

Related Questions