Making Google Maps work with React
tl;dr Embedding a Google Map on a webpage is trivial, embedding it in a ReactJS app is not. This post shows you one way to correctly do the async initialization.
Embedding a Google Map on a website should be a piece of cake. On a standalone HTML page it is: You add a <div
id="map"></div>
in the body and
one line of Javascript - et voilá,
you’re done. Recently, though, I wanted to use a Google Map in a ReactJS web app and found myself spending a
hideous amount of time troubleshooting several small problems.
There are plenty of articles that explain how to integrate Google Maps with React, in fact there are even several libraries that turn the Google Map and its overlay elements into React components. In contrast to most of the elaborate solutions out there, I will aim to explain the absolutely minimal setup I found necessary to get the two working together.
Play nice now, will ya?
The Problem
In a normal HTML file, you would init the Google Map like so:
<!DOCTYPE html>
<html>
<head>
<title>Basic Google Map on a web page</title>
</head>
<body>
<div id="map"></div>
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&"></script>
</body>
</html>
The Google Maps script is loaded after the static DOM has been initialized. When the script logic is executed, the
map div
element exists and is populated with the Google Map.
When writing a ReactJS app, things are not as simple. As you know, React operates on a performance-optimized virtual DOM, which only (re-)renders the actual DOM when state changes occur. A simple React app will usually look like this:
<!DOCTYPE html>
<html>
<head>
<title>Simple React app</title>
</head>
<body>
<div id="app"></div>
<script src="js/app.js"></script>
</body>
</html>
The app div
is populated once the React app has loaded and all components have been mounted. Now suppose that you want
to load the Google Map in one of your React components, which would render the <div id="map"></div>
element. If you
simply include the Google Maps script like in the above example, it won’t be able to find the map div
when its init
logic executes, because that div
hasn’t been rendered yet.
Asynchronous Loading
Since both the React app and the Google Maps take some time to load, we need to somehow ensure the Google Map is only created after the React app has been initialized and rendered into the DOM. At first glance, the asynchronous version of the Google Maps script seems to be a promising solution:
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
async defer></script>
Notice the keywords async defer
, which cause the script to be loaded only after all synchronous scripts have
loaded. Also notice the callback parameter in the
URL, which denotes the name of a global javascript function that should be invoked once the Google Maps script has
loaded. We could then programmatically instantiate the Google Map from an initMap()
function within our React
component:
function initMap() {
map = new google.maps.Map(document.getElementById('map'), { ... });
}
Does this work?
Nope, unfortunately not. Even though the Google Maps script is only loaded after the React app script starts to execute, this does not mean that the React app is fully mounted and rendered when the callback is invoked.
Real Asynchronous Loading
Ok, so we need something better. Essentially, we want to load the Google Maps script only once the component that
needs it has been mounted. How do we achieve that? By means of a script loading function (such as
loadJS) which is called from within componentDidMount()
.
module.exports = React.createClass({
...
componentDidMount: function() {
// Connect the initMap() function within this class to the global window context,
// so Google Maps can invoke it
window.initMap = this.initMap;
// Asynchronously load the Google Maps script, passing in the callback reference
loadJS('https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap')
},
initMap: function() {
map = new google.maps.Map(this.refs.map.getDOMNode(), { ... });
},
render: function() {
return (
<div>
...
<div ref="map" style="height: '500px', width: '500px'"><⁄div>
<⁄div>
);
}
});
function loadJS(src) {
var ref = window.document.getElementsByTagName("script")[0];
var script = window.document.createElement("script");
script.src = src;
script.async = true;
ref.parentNode.insertBefore(script, ref);
}
Notice the ref='map'
attribute in the markup, which enables us to reference the correct div
and pass it to the
Google Map constructor via this.refs.map.getDOMNode()
once the DOM has been rendered. You can think of React
references for the virtual DOM as the equivalent of id
references in a normal DOM.
That’s it. The Google map should now behave nicely and show up once your React component has been mounted!