I create things. I blog about it. Sometimes.

6 November 2016

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!