Possible to render react component within mapboxgl.Popup() in .setHTML or .setDOMContent?
Asked Answered
E

6

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!

Exhaust answered 21/2, 2018 at 22:53 Comment(0)
R
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);
Recoup answered 6/6, 2018 at 6:1 Comment(5)
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 <div> or similar node, render some JSX into it, and add the node as the DOM content for the popup.Earthshine
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.Hade
For instance, if you have a theme provider or a router implemented. This will surely mess up all this.Hade
@ChristianMoen how to use it and not lose providers and router? ThanksCopartner
@Copartner I'd use the wrapper github.com/visgl/react-map-gl or look at the implementation of popup there; github.com/visgl/react-map-gl/blob/master/src/components/…Hade
C
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;

Cuirass answered 3/6, 2020 at 5:36 Comment(4)
Do you have any examples on how you're using this component?Hade
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.Scyphozoan
@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. codesandbox.io/s/mapbox-react-popups-fd4d4?file=/src/App.jsScyphozoan
Nice find! Mostly worked for me. I added an answer with some tweaks that worked for me.Werth
W
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')
          );
        }
      });
Wahhabi answered 25/4, 2018 at 22:41 Comment(0)
D
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);
Daddylonglegs answered 29/11, 2020 at 14:6 Comment(0)
D
0

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://mcmap.net/q/748719/-adding-a-button-to-the-popup-in-mapboxgl-js

Disunity answered 6/5, 2021 at 8:52 Comment(0)
W
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>
}
Werth answered 27/3, 2022 at 21:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.