16

I am wondering if it is possible to render a react component within a mapboxgl.Popup(). Something like this:

    componentDidMount() {
            new mapboxgl.Popup()
            .setLngLat(coordinates)
            .setHTML(`<div>${<MapPopup />}<p>${moreText}</p></div>`)
            //.setDOMContent(`${<MapPopup />}`) ?????
            .addTo(this.props.mapboxMap)
    })

Or should this be done using ReactDOM.render?

ReactDOM.render(<MapPopup />, document.getElementById('root'))

This project will have buttons and inputs in the popup that connect to a redux store.

Thanks for any input!

jasonrberney
  • 161
  • 1
  • 6

6 Answers6

23

This works:

addPopup(el: JSX.Element, lat: number, lng: number) {
    const placeholder = document.createElement('div');
    ReactDOM.render(el, placeholder);

    const marker = new MapboxGl.Popup()
                        .setDOMContent(placeholder)
                        .setLngLat({lng: lng, lat: lat})
                        .addTo(map);
}

(Where I've used typescript to illustrate types, but you can just leave these out for pure js.) Use it as

addPopup(<h1>Losers of 1966 World Cup</h1>, 52.5, 13.4);
thund
  • 1,842
  • 2
  • 21
  • 31
  • 1
    Not sure why this wasn't marked as correct, but it should be. You don't necessarily need to use this wrapper function pattern but the basic idea is make a
    or similar node, render some JSX into it, and add the node as the DOM content for the popup.
    – Matt Webster Mar 10 '21 at 22:03
  • 2
    Why should this be marked as the correct answer? You loose all of your providers by doing it like this. And it's not the React way. – Christian Moen Sep 14 '21 at 08:50
  • 1
    For instance, if you have a theme provider or a router implemented. This will surely mess up all this. – Christian Moen Sep 14 '21 at 08:53
  • 1
    @ChristianMoen how to use it and not lose providers and router? Thanks – SERG Jun 05 '22 at 08:48
  • 1
    @SERG I'd use the wrapper https://github.com/visgl/react-map-gl or look at the implementation of popup there; https://github.com/visgl/react-map-gl/blob/master/src/components/popup.ts – Christian Moen Jun 06 '22 at 10:37
6

You can try to implement React component:

export const Popup = ({ children, latitude, longitude, ...mapboxPopupProps }) => {
    // this is a mapbox map instance, you can pass it via props
    const { map } = useContext(MapboxContext);
    const popupRef = useRef();

    useEffect(() => {
        const popup = new MapboxPopup(mapboxPopupProps)
            .setLngLat([longitude, latitude])
            .setDOMContent(popupRef.current)
            .addTo(map);

        return popup.remove;
    }, [children, mapboxPopupProps, longitude, latitude]);

    return (
        /**
         * This component has to have 2 divs.
         * Because if you remove outter div, React has some difficulties
         * with unmounting this component.
         * Also `display: none` is solving that map does not jump when hovering
         * ¯\_(ツ)_/¯
         */
        <div style={{ display: 'none' }}>
            <div ref={popupRef}>
                {children}
            </div>
        </div>
    );
};

After some testing, I have realized that Popup component was not rendering properly on the map. And also unmounting the component was unsuccessful. That is why there are two divs in return. However, it may happen only in my environment.

See https://docs.mapbox.com/mapbox-gl-js/api/#popup for additional mapboxPopupProps

useEffect dependencies make sure that MapboxPopup gets re-created every time something of that list changes & cleaning up the previous popup instance with return popup.remove;

Jan Dočkal
  • 89
  • 1
  • 8
  • 1
    Do you have any examples on how you're using this component? – Christian Moen Sep 14 '21 at 09:22
  • This really ought to be the correct answer. Unlike the other answers that require you to create a new react app in a react app, this one doesn't break context so that you can have redux or other context actions take place in the popup. – brianbancroft Nov 06 '21 at 15:04
  • 1
    @ChristianMoen here's a buggy, but working example. I'm going to stop here, but aside needing to reset the content on close, the interactivity works. https://codesandbox.io/s/mapbox-react-popups-fd4d4?file=/src/App.js – brianbancroft Nov 08 '21 at 15:03
  • Nice find! Mostly worked for me. I added an answer with some tweaks that worked for me. – WebSpence Mar 27 '22 at 21:23
2

I've been battling with this as well. One solution I found was using ReactDOM.render(). I created an empty popup then use the container generated by mapboxgl to render my React component.

    marker.setPopup(new mapboxgl.Popup({ offset: 18 }).setHTML(''));


     markerEl.addEventListener('mouseenter', () => {
        markerEl.classList.add('enlarge');

        if (!marker.getPopup().isOpen()) {
          marker.getPopup().addTo(this.getMap());

          ReactDOM.render(
            component,
            document.querySelector('.mapboxgl-popup-content')
          );
        }
      });
dunncl15
  • 21
  • 3
0
const mapCardNode = document.createElement("div");

mapCardNode.className = "css-class-name";

ReactDOM.render( 
  <YourReactPopupComponent / > ,
  mapCardNode
);

//if you have a popup then we remove it from the map
if (popupMarker.current) popupMarker.current.remove();

popupBox.current = new mapboxgl.Popup({
        closeOnClick: false,
        anchor: "center",
        maxWidth: "240px",
    })
    .setLngLat(coordinates)
    .setDOMContent(mapCardNode)
    .addTo(map);
nntona
  • 409
  • 4
  • 9
0

I used MapBox GL's map and popup events (to improve upon @Jan Dockal solution) which seemed to improve reliability. Also, removed the extra div wrapper.

import { useWorldMap as useMap } from 'hooks/useWorldMap'
import mapboxgl from 'mapbox-gl'
import { FC, useRef, useEffect } from 'react'

export const Popup: FC<{
  layerId: string
}> = ({ layerId, children }) => {
  const map = useMap() // Uses React Context to get a mapboxgl map (could possibly be null)
  const containerRef = useRef<HTMLDivElement>(null)
  const popupRef = useRef<mapboxgl.Popup>()

  const handleClick = (
    e: mapboxgl.MapMouseEvent & {
      features?: mapboxgl.MapboxGeoJSONFeature[] | undefined
    } & mapboxgl.EventData
  ) => {
    // Bail early if there is no map or container
    if (!map || !containerRef.current) {
      return
    }

    // Remove the previous popup if it exists (useful to prevent multiple popups)
    if (popupRef.current) {
      popupRef.current.remove()
      popupRef.current = undefined
    }

    // Create the popup and add it to the world map
    const popup = new mapboxgl.Popup()
      .setLngLat(e.lngLat) // could also use the coordinates from a feature geometry if the source is in geojson format
      .setDOMContent(containerRef.current)
      .addTo(map)

    // Keep track of the current popup
    popupRef.current = popup

    // Remove the tracked popup with the popup is closed
    popup.on('close', () => {
      popupRef.current = undefined
    })
  }

  useEffect(() => {
    if (map && layerId) {
      // Listen for clicks on the specified layer
      map?.on('click', layerId, handleClick)

      // Clean up the event listener
      return () => {
        map?.off('click', layerId, handleClick)
        popupRef.current?.remove()
        popupRef.current = undefined
      }
    }
  }, [map, layerId])

  return <div ref={containerRef}>{children}</div>
}
WebSpence
  • 147
  • 2
  • 10
-1

Try to do with onClick event, instead of creating a button. After that put your react component in onClick events add event listener refrence link [1]: https://stackoverflow.com/a/64182029/15570982

Abdurakhmon
  • 2,813
  • 5
  • 20
  • 41
Jerry
  • 30
  • 1