Search code examples
reactjsleafletreact-leafletreact-leaflet-v3

How to open a specific popup on map load?


Using React-Leaflet V3.1.0 with an Express server.

My users have requested the ability to use a link in the following format:

https://www.mymap.com/id/:category/:id/:zoom

This sends a request to the server to get the lat/lon coordinates of the marker they're linking to and then sets the start location to those coordinates. I've managed to get this part working using React Router Dom. Now ideally, I would like to be able to have the popup for this particular marker opened upon map load. With the way I have the query component setup, it's not a problem to be able to pass a boolean value through props while generating the markers to indicate that this is the marker of interest, I'm just not sure how to activate the popup.

I'm not very familiar with useRef and how it can be used exactly to access the different Leaflet methods but I did some experiments yesterday and was able to get some behavior that seems to be a step in the right direction. The popups didn't load on initial startup but as I navigated around the map, other popups started opening automatically. That being said, my map renders thousands of markers and going beyond 1 render per marker isn't going to be worth the trade off of this relatively niche feature, unless a second render can be isolated to only the marker of interest.

The method that I think would get me to where I want but I couldn't get this specific one to fire: https://leafletjs.com/reference-1.6.0.html#popup-openon

I ended up using openPopup() similar to this link: https://stackblitz.com/edit/react-awtgn6?file=Map.js

But I was only able to get that to somewhat (results mentioned above) work with a useEffect + useRef.

I feel like I'm missing something obvious when it comes to storing the popup open status in state?

Basic code example:

<Marker
  position={[item.lat, item.lon]}
  icon={getIcon(item)}
>
  <Popup position={[item.lat, item.lon]}>
    <PopupContent
      item={item}
      userSettings={userSettings}
    />
  </Popup>
</Marker>

Thank you!


Solution

  • If the goal is to get a marker's popup to open, on the condition that the marker's id is specified in the url param (i.e. url?id=3), this is doable.

    Multiple refs in a ref object

    You'll want to map over an array of markers, each with a unique id. You'll need to keep an array or object of refs, and as you map, in the ref prop, add the ref to that array of object:

    const Map = (props) => {
      const [done, setDone] = React.useState(false);
      const markerRefs = React.useRef({});
    
    return (
        <MapContainer {...mapContainerProps}>
          {data.map((marker, index) => {
            return (
              <Marker
                position={marker.coord}
                ref={(m) => {  // <--- add marker refs to ref object here
                  markerRefs.current[marker.id] = m;
                  if (index === data.length - 1 && !done) {
                    setDone(true);
                  }
                }}
              >
                <Popup>{marker.content}</Popup>
              </Marker>
            );
          })}
        </MapContainer>
    

    After your map statement executes, your markerRefs will be an object, whose keys are the ids, and whose values are the underlying leaflet marker objects.

    refs !== state

    You'll need a way to take some action once the refs are ready. You can use a useEffect to do that. Now keep in mind that putting a ref inside a useEffect dependency array is not a good idea, as an update to the ref will not trigger a rerender. So instead, I created the state variable done, which starts as false, but is set to true on the last iteration of the map.

    Open the popup

    Now, we create a useEffect which runs when done is true, which means the refs are created and available:

      useEffect(() => {
        let params = new URLSearchParams(document.location.search.substring(1));
        let id = params.get("id");
        if (id && done) {
          const markerToOpen = markerRefs.current[id];
          markerToOpen.openPopup();
        }
      }, [done]);
    

    In this useEffect, we check the url params to get the id. If there's an id, and the mapping and reffing is done, we'll get the marker ref according to the marker's id, and we'll call openPopup() on it.

    Working codesandbox

    Keep in mind that sandbox type websites like codesandbox or stackblitz have iframes, and the url param needs to be available there. For that sandbox, you either need to pop out the preview into its own window, or use the url for the iframe:

    enter image description here

    You'll see that when you specify the id in the url search params, that marker's popup is open on mount. (I didn't write any code to check that the marker exists, so if you try id=doesnt_exist in that example, it will break. You'll need to add extra logic to check for that).

    It will take some extra work to integrate this with react-router and the other url params you want to use, but that's how I would go about using a combination of refs and state to reach through react to the underlying leaflet components and accomplish what you want.

    As far as rerenders go with many markers, keep in mind that react-leaflet 3 is optimized not to cause rerenders, but to reach into the underlying leaflet components and call their methods. You may want to create other state variables to track to help to this. For example, you might have const [currentOpenMarker, setCurrentOpenMarker] = useState(), and put a usEffect on that to reach into the underlying leaflet map and call map.closePopup() and then currentOpenMarker.openPopup().