Decoupling from Google Maps

Adapted from a technical talk I gave at Frontend York about using the Google Maps JS badly on a website, the technical debt that can be accrued, and a few different modern techniques to use it well. Full code is available here.

When we rely on Google Maps for it’s API (I.e. geocoding addresses, Places API) it becomes a hard dependency on your JS code that becomes a legacy problem and hard to remove.

We’ve all seen and probably worked on single page apps that use Google Maps, ones where you have some data to display. These all load the Google Maps Javascript from the Google servers, take some of your data, and then show it on the maps, usually as markers.

You are relying on

  • The internet connection between you and Google servers to work;
  • Google’s servers to be nice and give you the Javascript;
  • Your browser to download, parse and then execute that code and your application’s code.

How Google told you to use their code 2005 - 2015 (ish)

<head>
  ...
  <script type="text/javascript" src="//maps.googleapis.com/maps/api/js?key=KEY"></script>
</head>

<div id="map"></div>

<script>
  var map = new google.maps.Map(document.getElementById('map'), {
    ...
  })
  window.google.maps.GeolocationService.geocode("10 Downing Street, London")
</script>

The problems with using this style of code in your large modern JS application might not be immediately clear, but…

  • This is on the HTML page, and not with the rest of our JS code. This is outside of our (imaginary) webpack’ed application;
  • The Google Maps JS is loaded syncronously - it waits for the page to finish parsing before it begins to download it;
  • The initMap callback triggered on load by the Maps JS effectively becomes where the code execution starts from. Which is bad. We want our code to run independently of Google telling us to run it.

Let’s defer the script

To resolve the Google Maps JS being loaded syncronously and slowing down our page, we could defer the loading until after the page has loaded, or load it asyncronously so that our page doesn’t wait for the JS to complete it’s download before rendering.

<div id="map"></div>

<script>
  function initMap() {
    var map = new google.maps.Map(document.getElementById('map'), {
      ...
    })

    window.google.maps.GeolocationService.geocode("10 Downing Street, London")
  }
</script>

<script src="//maps.googleapis.com/maps/api/js?key=KEY&callback=initMap"
  async defer>
</script>

But now we’ve made things even worse -

  • The JS isn’t loading in a predictable order. Any other JS files that interact with our map wont work
  • window.google.maps is not available when the page first loads
  • initMap function becomes the entry point for our entire app. Everything in our app has to be done in here

Because window.google.maps isn’t immediately available to us on page load, we need to use callback initMap - that the Maps JS runs once it has completed downloading and is ready. All our code then has to be loaded from that callback in order to work properly.

Image showing an Airbrake error. window.google not available on page load

This is coupling

Our code is now tightly coupled to the window.google.maps variable and permanently relies on the Google Maps JS on running our callback. We have an interdependence on the Maps JS library, one that will remain until we address the underlying problems I’ve identified here.

But does it matter?

Modern large apps that display data on a Map have lots of components, lots of moving parts. If we rely on Google’s JS in this way (or, indeed any third party library) then Google Maps is our app, and not just a component that we can use.

We’ll find that other components that interact with our map will rely on window.google and not work without it, so we’ll write them for the Google Maps JS, rather than us just using it as an external library.

So what can we do about it?

Vanilla HTML component

We can use modern ES6 promises to await the Google JS before displaying the map using a custom parameter.

const gmap = async (e) => {
  return new Promise(async fulfilled => {
    e.style.height = e.style.width = e.getAttribute('size')
    let latlng = e.getAttribute('latlng').split(',')

    await fetch('https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&v=3')
    let map = new google.maps.Map(e, {
      center: new self.google.maps.LatLng(latlng[0], latlng[1]),
      zoom:   14
    });

    fulfilled(map)
  })
}

document.addEventListener("DOMContentLoaded", function(event) {
  let controls = document.querySelectorAll('div[control=gmap]')
  Promise.all(
    controls.map(control => {
      gmap(control)
    })
  )
})

And then we load it with

<div control='gmap' size='200' latlng='53.9623241,-1.1169397'></div>

Now we’re

  • Async loading the script, only once (which we were doing anyway)
  • google.maps is encapsulated within the gmap function, we’re not using it elsewhere so we can’t get errors about it not existing
  • We can fully control when the map is loaded - I’ve used an event listener for DCL, but you could add it for button presses, or on search results being shown dynamically for instance

W3C Web Component

W3C started to standardise this idea in 2014, and have a proposed standard for Web Components into reusable and modular components. It means we can combine vanilla JS, HTML & CSS and instantiate it through custom HTML tags -

class Map extends HTMLElement {...}
window.customElements.define('map', Map);

Then we can use our new custom HTML tag for our component

<map>...</map>

As the standard isn’t fully drafted yet, I won’t go much into this, other than it’s a thing. See more about it here

React component

import React, { Component } from 'react'
import { withScriptjs, withGoogleMap, GoogleMap, Marker } from "react-google-maps"

const MapComponent = withScriptjs(
  withGoogleMap((props) =>
    <GoogleMap defaultZoom={10} defaultCenter={ lat: 53.9623241, lng: -1.1169397 }>
      <Marker position={ lat: 53.9623241, lng: -1.1169397 } />
    </GoogleMap>
  ))

class App extends Component {
  render() {
    return (
      <div>
        <header>
          <h1>React Google Maps</h1>
        </header>

        <MapComponent googleMapURL="https://maps.googleapis.com/maps/api/js?v=3" />
      </div>
    );
  }
}

react-google-maps is a mature React component that’s ready to use and comes ready componentised. It also allows for

  • Compiled scripts handling the markers and async loading - we’re not re-writing them
  • The data for our markers is out of the DOM entirely, and within the state system
  • The Map class is a component and can be reused in the same way
  • Interactions with the Map class becoming components in their own right, allowing for seperating out user actions from the logic, for example

Full code is available here