Andy Callaghan

Decoupling from Google Maps JS

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

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...

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 -

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

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

Full code is available here