Using Google Maps in React without custom libraries

There is no official React library from Google for Google Maps, but there is a JS API. So, if you don't like to use an unofficial custom library but still want to use it with React, you can still use it with some little tweaks. Using an unofficial npm library means you're dependent on it, you need to learn its API and you cannot use community help efficiently.

SEE LAZY LOADING DEMO
SEE REACT INFOWINDOW DEMO

Anyway, there are 2 main steps you need to handle in order to use Google Maps in React.

  1. Lazy loading Google Maps library
  2. Mounting your React component inside the maps

1. Lazy Loading Google Maps Library


Actually, the first step could be skipped if you use maps synchronously. Using synchronous means, the first time when you load the page it will be slightly slower. But maps library will be loaded globally so you can access `window.google.maps`, `window.google.maps.Marker` etc. anywhere in your application.

index.html

<head>
  <script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY" />
</head>


And then in your react component simply call the API:

class Map extends Component {
  componentDidMount() {
    const map = new window.google.maps.Map(document.getElementById('map'), {
      center: { lat: 41.0082, lng: 28.9784 },
      zoom: 8
    });
  }

  render() {
    return (
      <div style={{ width: 500, height: 500 }} id="map" />
    );
  }
}

But if you want to create a reusable React component that's imported into your codebase with something like import Map from './map', you need to do lazy loading.

Once you load the script and get the reference to the map you've created that's it you can use Google Maps Javascript API limitlessly. I want to keep it as simple as possible for the tutorial but it can be extended as you wish.

map.js:

import React, { Component } from 'react';
import { render } from 'react-dom';

class Map extends Component {
  constructor(props) {
    super(props);
    this.onScriptLoad = this.onScriptLoad.bind(this)
  }

  onScriptLoad() {
    const map = new window.google.maps.Map(
      document.getElementById(this.props.id),
      this.props.options);
    this.props.onMapLoad(map)
  }

  componentDidMount() {
    if (!window.google) {
      var s = document.createElement('script');
      s.type = 'text/javascript';
      s.src = `https://maps.google.com/maps/api/js?key=YOUR_API_KEY`;
      var x = document.getElementsByTagName('script')[0];
      x.parentNode.insertBefore(s, x);
      // Below is important. 
      //We cannot access google.maps until it's finished loading
      s.addEventListener('load', e => {
        this.onScriptLoad()
      })
    } else {
      this.onScriptLoad()
    }
  }

  render() {
    return (
      <div style={{ width: 500, height: 500 }} id={this.props.id} />
    );
  }
}

export default Map

Now in the page, initialize the map with your custom props:

render() {
    return (
      <Map
        id="myMap"
        options={{
          center: { lat: 41.0082, lng: 28.9784 },
          zoom: 8
        }}
        onMapLoad={map => {
          var marker = new window.google.maps.Marker({
            position: { lat: 41.0082, lng: 28.9784 },
            map: map,
            title: 'Hello Istanbul!'
          });
        }}
      />
    );
  }

And now let's explain what we've done so far:

Map component adds a div with an id assigned from the parent container. When the component mounted, we insert a script node to the parent element (or we can add it to the head). This cannot be synchronous. So, if you try to access the API you will get an error. We need to add an event listener for 'load'. At this point, we know that we can access the API. But we don't want to load the same library again if the API is already included. So, simply wrap it with an if (!window.google) phrase.

After accessing the API, we can now create a map. Call the API to create a map with the options. Options are sent as props from the parent. After creating the map, the parent will need to get the reference to the map. That's why we've added onMapLoad prop. This is a func prop that will be called when the map is created and will receive the map as a parameter. Using this parameter we can now create Markers, InfoWindow, Polygons and whatever the Maps API has. In the example, it creates a simple marker on the map.

You can see above code snippet working online: SEE LAZY LOADING

2. Mounting React components inside the maps


What we've achieved so far is pretty good but not enough if you want to add React components inside the map. This can be either an InfoWindow or a Custom component (eg. search box, filters etc.). You can create an InfoWindow from HTML snippet; but if you want to add a component (eg. button) that dispatches a Redux action, it will not be very straightforward. You need to mount the React component inside the DOM element using react-dom. That's the same action when we've mounted the whole React App inside <div id='root' />

So, let's extend our first application and mount a React component as the Info Window when the user clicks the Marker.

To demonstrate let's mount a material-ui Card into an InfoWindow. Our InfoWindow will be this card: https://material-ui-next.com/demos/cards/

Our map.js component will stay as is. We've created a reusable component right? ;) We can extend our container as below:

createInfoWindow(e, map) {
    const infoWindow = new window.google.maps.InfoWindow({
        content: '<div id="infoWindow" />',
        position: { lat: e.latLng.lat(), lng: e.latLng.lng() }
    })
    infoWindow.addListener('domready', e => {
      render(<InfoWindow />, document.getElementById('infoWindow'))
    })
    infoWindow.open(map)
  }

  render() {
    return (
      <Map
        id="myMap"
        options={{
          center: { lat: 41.0082, lng: 28.9784 },
          zoom: 8
        }}
        onMapLoad={map => {
          const marker = new window.google.maps.Marker({
            position: { lat: 41.0082, lng: 28.9784 },
            map: map,
            title: 'Hello Istanbul!'
          });
          marker.addListener('click', e => {
            this.createInfoWindow(e, map)
          })
        }}
      />
    );
  }

API only allows stringified HTML snippets for InfoWindow. If all you need is HTML, then you don't need to do this. But, if you want to use everything that React/Redux offers, you need to do the mounting. First, we add an empty div with an id inside the InfoWindow. When the InfoWindow finishes loading, this div becomes an accessible DOM element. Now, React can do its magic.

So, here is our React InfoWindow:

  import React from 'react';
import { withStyles } from 'material-ui/styles';
import Card, { CardActions, CardContent, CardMedia } from 'material-ui/Card';
import Button from 'material-ui/Button';
import Typography from 'material-ui/Typography';

const styles = {
  card: {
    maxWidth: 345,
  },
  media: {
    height: 0,
    paddingTop: '56.25%',
  },
};

function InfoWindow(props) {
  const { classes } = props;
  return (
    <div>
      <Card className={classes.card}>
        <CardMedia
          className={classes.media}
image="https://upload.wikimedia.org/wikipedia/commons/thumb/b/b8/Bosphorus.jpg/397px-Bosphorus.jpg"
          title="Contemplative Reptile"
        />
        <CardContent>
          <Typography gutterBottom variant="headline" component="h2">
            Istanbul
          </Typography>
          <Typography component="p">
            Istanbul is a major city in Turkey that straddles Europe and Asia across the Bosphorus Strait. Its Old City reflects cultural influences of the many empires that once ruled here.

          </Typography>
        </CardContent>
        <CardActions>
          <Button size="small" color="primary">
            Share
          </Button>
          <Button size="small" color="primary">
            Learn More
          </Button>
        </CardActions>
      </Card>
    </div>
  );
}


export default withStyles(styles)(InfoWindow);

CHECK THIS EXAMPLE ONLINE

These are the 2 necessary steps you need to do in order to use Google Maps Javascript API in React. The rest is just extending or repeating these steps. There are some libraries you can use like react-google-maps. It has a good reputation, but I don't like using unofficial libraries where I can create the same using little effort.