javascriptreactjsleafletleaflet.markerclusterreact-leaflet-v3

react-leaflet: Clear marker cluster before rendering new markers


TLDR; Is there a way to clear all markers from a marker cluster using react-leaflet and react-leaflet-markercluster?

EDIT: Yes, there is! I just added a key prop to the MarkerClusterGroup, as @SethLutske recommended. Now, every time there is a re-render of the map, the previous markers are removed and only the new ones remain.

<MarkerClusterGroup
    key={uuidv4()}  
    spiderfyDistanceMultiplier={1}
    showCoverageOnHover={true}
>

I work in the Public Security agency of my state. There is an API that gets a collection of monitoring cameras from a desired city of my State. I'm working in a React PWA that retrieves the image of these cameras, so the police can use my app for live monitoring. The policemen authenticate into the system, filter the cameras by their city using a form, and the result is shown in a map with markers that indicate the position of each camera. When touching a marker, a popup is shown with information of that camera and a button that shows the camera image in a new page when touched.

Map markers and popup

I chose Leaflet and react-leaflet for the map rendering, and react-leaflet-markercluster to group the markers that are next each other. I'm having a little problem: if an user filters cameras by a city, then he returns to the form and decides to filter by a different city, the behavior that I wanted to happen is:

But what happens is this: the markers from the previous city remain in the map, with an empty popup.

Marker groups from different cities

MY GOAL: I want to clear the past city markers before adding the markers of a new city!

Here is how it is my MapContainer. It is working quite well, despite of the bug I mentioned.

      <MapContainer            
          center={[-26.627817, -51.196288]}
          zoom={6}
          scrollWheelZoom={false}
          style={{ height: '100vh', width: '100wh' }}
      >
        {//<ClearLayers /> this clears the entire map, even the tile layer, leaving it blank
        }
          <TileLayer
          attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
          />
          <MarkerClusterGroup
              spiderfyDistanceMultiplier={1}
              showCoverageOnHover={false}
          >
              {cameras.map((camera: Camera) => (
                  <Marker icon={iconBTV} key={camera.name} position={[camera.latitude, camera.longitude]}>
                      <Popup>
                        {camera.name} <br /> {camera.description} <br />
                        <Link
                          to={{
                            pathname: "/cameraScreen",
                          }}
                          style={{ textDecoration: 'none' }}
                        >
                          <button
                            style={{marginTop: "1rem"}}
                            onClick={(e) => insertLog(e, camera)}
                          >Ver câmera</button>
                        </Link>                            
                      </Popup>
                  </Marker>
              ))}
          </MarkerClusterGroup>
      </MapContainer>

I tried using this function, but it doesn't work as I wanted. It bugs the map, leaving it completely blank.

function ClearLayers() {
  const map = useMap();
  map.eachLayer(function (layer) {
    map.removeLayer(layer);
  });
  return null
}

I've searched in here and came close to a resolution: I was able to clear the past city markers by using Leaflet's core API and leaving behind react-leaflet-markercluster wrapper as shown in this answer. I do execute clearLayers() before adding new markers to the MCG, and that does the trick. Here's what I tried to do:

    import * as L from 'leaflet';
    import 'leaflet.markercluster';

    import { useMap } from 'react-leaflet';
    import { useContext, useEffect } from 'react';
    import { Link } from 'react-router-dom';
    import CameraContext from '../../contexts/CameraContext';

    const mcg = L.markerClusterGroup({
        chunkedLoading: true,
        showCoverageOnHover: false,
    });

    const MarkerCluster = ({ markers, icon }) => {
        const { setCamera } = useContext(CameraContext);

        const inserirLog = (e, camera) => {
            setCamera(camera);
            var ajax_log = new XMLHttpRequest();
            ajax_log.open('GET', `https://fakeurl.sc.gov.br/api/ajax-log-bemtevi.php?camera=${camera.nome}&servidor=${camera.servidor}`);
            ajax_log.send();
        }   

        console.log(markers);
        const map = useMap();
        useEffect(() => {
            mcg.clearLayers();
            const markerList = markers.map((camera) => {
                return L.marker(new L.LatLng(camera.latitude, camera.longitude), {
                    icon: icon,
                }).bindPopup(
                    `
                    ${camera.name} <br /> ${camera.description} <br />
                    <Link
                    to={{
                        pathname: "/cameraScreen",
                    }}
                    style={{ textDecoration: 'none' }}
                    >
                    <button
                        style={{marginTop: "1rem"}}
                        onClick={(e) => insertLog(e, camera)}
                    >Watch camera</button>
                    </Link>
                    `
                );
            });
            mcg.addLayers(markerList);
            map.fitBounds(mcg.getBounds());
            map.addLayer(mcg);
        }, [markers, icon, map]);

        return null;
    };

    export default MarkerCluster;

By doing that, I can't inject JSX inside the Popup, and can't use the Link component from react-router-dom. I also couldn't save the camera object in a context to use it further in the next page. So I ended sticking to my current code, that is working, despite of having this "can't clear past city markers" bug.

So I'd like to have a way to clear the past city markers when using the React components for Leaflet map and Marker Cluster. If there was a hook for the MarkerClusterGroup like there is one for the Leaflet map, I guess this would be easier to do: all I would need to do is something simple as this:

    const markercluster = useMarkerClusterGroup();
    markercluster.clearLayers();

But, as long as I know, there aren't hooks in react-leaflet-markercluster.


Solution

  • You can add a key prop to MarkerClusterGroup, and change it when you want the cluster to be completely rerendered.

    <MarkerClusterGroup
      key={somekey}
      spiderfyDistanceMultiplier={1}
      showCoverageOnHover={false}
    >
    

    I'm not sure what logic is triggering the markers to change, but within that same logic, when the markers change, generate a new unique value (whether its a simple counter that increments, or a new uuid), set the key to that new value, and the entire component should unmount and remount. While this isn't great for performance if there are a huge number of markers, it sounds like what you're trying to do.