What is this article about ?
How to render a react component as a popup on google map: a perfect use case for React portal. I'll briefly describe the drawing api provided by google map javascript library and then talk about how I integrated this 3rd party library with react.
PS. Some implementation details have been removed from the code blocks to improve readability. The complete working example can be found on the corresponding codepen links.
About React Portals
React Portals is this shiny new API from react .. not really. React introduces this api in v16.0 and there are tons of articles with very elaborate and interesting explanations about what it is all about. So won't be talking much about it here. Just google react portal, or check these out..
https://css-tricks.com/using-react-portals-to-render-children-outside-the-dom-hierarchy
https://programmingwithmosh.com/javascript/using-react-portals/
https://codeburst.io/reacts-portals-in-3-minutes-9b2efb74e9a9
About Google maps drawing api.
Skip this section if already familiar with the google maps api.
Google Maps library provides apis to draw any custom HTML content on the map. InfoWindow is one of the popular apis. Another less known option is the OverlayView class. In general InfoWindow apis are easy to use so implementations are much faster, but the popup markup is not fully customizable.
In one of my previous projects in react, I wanted to render a react component in the map as an overlay and found that the OverlayView interface allows to plug-in any HTML element/markup on the map. This write-up will focus on this OverlayView
class, although, the concepts discussed here can be applied to InfoWindow based implementation as well.
To render an overlay on the map using the OverlayView class, we need to implement a class which extends the OverlayView class. Here we need to flesh out 3 main functions.
-
onAdd
Append HTML element to a container element in the rendered map canvas. This method is invoked once, when map attempts to render the overlay on the canvas. -
draw
Set the x, y position of the overlay content. The position (x, y) of the element is translated from the lat, lng values for the position. This method is called each time the content needs to be updated on the map. For example map zoom or pan. -
onRemove
Removes the element from the map. Called when it is time to remove/hide the overlay element from the map canvas.
Refer to the Custom Popups example and Custom Overlay guide for detailed explanation about the apis and methods.
To kick-off, let's create a React component to render a map.
The implementation is quite straight forward, so let's dive right in.
See codepen example.
class Map extends React.Component {
/** Map instance */
map = null;
/** DOM container where the map canvas gets rendered. */
mapContainer = React.createRef();
componentDidMount() {
/** Create new google map. */
this.map = new google.maps.Map(this.mapContainer.current, {
zoom: this.props.zoom,
center: this.props.center
})
}
render() {
// reference to the DOM element where the map will be rendered
return <div ref={this.mapContainer}
style={{ height: '100vh', width: '100vw'}}></div>
}
}
ReactDOM.render(<Map />, document.getElementById('root'))
The important thing to note here is that the map is rendered inside the container element.
All the markup inside the container is generated and controlled by the map library. React has no control or visibility of the DOM in the map canvas.
Now let's draw a popup in the map.
Here is a codepen with a react component to draw custom HTML markup on the map.
Fo discussion purposes, I have laid out important parts of the CustomOverlayView
class that extends the google.maps.OverlayView
class.
class CustomOverlayView extends window.google.maps.OverlayView {
constructor(props) {
super(props);
this.position = props.position; // lat, lng position provided by map. This is where the popup is supposed to be rendered
this.content = props.content; // The HTML element to be rendered in the popup.
// Create and style the popup markup.
this.containerDiv = document.createElement("div");
this.containerDiv.appendChild(content);
}
/** Called when the popup is added to the map. */
onAdd = () => {
this.getPanes().floatPane.appendChild(this.containerDiv);
};
/** Called when the popup is removed from the map. */
onRemove = () => {
this.content.parentElement.removeChild(this.content);
};
/** Called each frame when the popup needs to draw itself. */
draw = () => {
const divPosition = this.getProjection().fromLatLngToDivPixel(
this.position
);
this.content.style.left = divPosition.x + "px";
this.content.style.top = divPosition.y + "px";
};
}
All that's left, is to create the overlay instance and attach it to the map by calling the setMap
method in the OverlayView
class.
Here is the updated componentDidMount
of the Map
component.
class Map extends React.Component {
...
componentDidMount() {
/** Create new google map. */
this.map = new google.maps.Map(this.mapContainer.current, {
zoom: this.props.zoom,
center: this.props.center
});
/** Create the overlay instance */
this.popup = new CustomOverlayView({
position: new google.maps.LatLng(
this.props.center.lat,
this.props.center.lng
),
content: document.getElementById("content")
});
// Attach the overlay instance to the map.
// This renders the overlay on the map.
this.popup.setMap(this.map);
}
}
To create the popup element, we need to provide a DOM element to the OverlayView class.
This element is then appended to a container element in the map,
this.getPanes().floatPane.appendChild
. SeeonAdd
method of theCustomOverlayView
class implementation.
Now comes the weird part.
To build the HTML content of the popup, we are getting an element from the DOM using document.getElementById("content")
. The react folks may find this uncomfortable.
Ideally this should be a react component rendered by react.
Examples provided by google use document.createElement('div')
, el.classList.add('popup-container')
approach to create the HTML markup manually.
Both ways of creating the popup markup are very non-react way of working with the DOM.
Let's do it the react way then.
I want to build the popup as a react component and have it rendered as part of the react virtual DOM.
This way, any updates are seamlessly propagated to the popup component via props. Just like any other react component.
The resulting jsx should look something like this...
<Map>
<OverlayViewContainer
position={{ lat: lat1, lng: lng1 }}>
{/* my custom popup react component */}
<CustomPopupComponent ... />
</OverlayViewContainer>
</Map>
The OverlayViewContainer
component can encapsulate all the wiring required to integrate our custom CustomOverlayView
class with react component tree and can render our custom react component CustomPopupComponent
on the map at a given position
.
This way we can have a clean interface to render react components on the map.
Show me the code!
Here is the sample app in the codepen. It renders a react component as an popup overlay on the map. The popup shows the current time, which updates every second.
A small update before jumping into the OverlayViewContainer
component.
OverlayViewContainer needs the map instance where the overlay will be rendered. We can use React.Context api to pass the map instance from the <Map>
to <OverlayViewContainer>
.
// New context for passing down the map object from `Map` component to its children.
const MapContext = React.createContext(null);
...
class Map extends React.Component {
...
render() {
return (
<div
ref={...} style={...}>
{/** Render the children and wrap them with MapContext.Provider component. */}
<MapContext.Provider value={this.map}>{this.props.children}</MapContext.Provider>
</div>
);
}
}
Finally!! let's build the magic component.
class OverlayViewContainer extends React.Component {
overlay = null;
el = null;
render() {
return (<MapContext.Consumer>{map => {
if (map) {
/**
* `OverlayView` will gracefully take this element
* and place it in a container on the map.
* This element will act as the host for
* the child popup component to be rendered.
* */
this.el = this.el || document.createElement('div');
/**
* Create the custom overlay view instance,
* that renders the given `content` at the given `position`
* */
this.overlay = this.overlay ||
new CustomOverlayView({
position: this.props.position,
content: this.el
});
this.overlay.setMap(map);
/**
* -----------------------------------------
* This where React.Portal works its MAGIC.
* The portal connects `this.props.children`
* with `this.el`.
* So anything in `this.props.children`
* gets rendered inside `this.el` DOM element.
* -----------------------------------------
* */
return ReactDOM.createPortal(this.props.children, this.el);
} else {
return null;
}
}}</MapContext.Consumer>);
}
}
OverlayView
will gracefully take this.el
element and place it in a container on the map.
this.el
will then act as the host for the child popup component to be rendered.
Here React.Portal works its magic by rendering this.props.children
inside this.el
(an element that inside the google map canvas.).
Anything rendered as children of OverlayViewContainer
, are still part of the react component hierarchy, despite being placed somewhere else on the DOM.
Its like opening a portal in the react component tree which connects to some other place on the DOM.
Any component passing through this portal will be rendered at that place on the DOM.
The resulting VDOM looks like this.
<Map>
<MapContext.Provider>
<OverlayViewContainer>
<MapContext.Consumer>
<CustomPopupComponent {...} />
// The component exists in the react component heirarchy
// but is rendered elsewhere on the actual DOM.
</MapContext.Consumer>
</OverlayViewContainer>
</MapContext.Provider>
</Map>
Done!
Thatโs a wrap! Hopefully this serves as a good example to demonstrate how React Portals can be used to render or affect DOM outside the DOM hierarchy.
It's common to think that a UI library that renders some HTML markup on the DOM, controls everything related to that markup, and nothing else. But React Portal api allows you to extend this functionality to anywhere on the DOM.
Thanks for reading.
Top comments (0)