Load Google Place API in Gatsbyjs (Reactjs) project

You shouldn't modify any files in the public forlder with GatsbyJS.

Instead, I recommend you to customize your html.js file.

To do so, first run:

cp .cache/default-html.js src/html.js

You should have the html.js file in /src/html.js.

Now you can put your <script> tag within the <head>.


Update Feb 24, 2020

Here's a more modern implementation using React hooks with some performance optimizations based on React.memo and a custom shouldUpdate function. See this blog post for details.

import { functions, isEqual, omit } from 'lodash'
import React, { useState, useEffect, useRef } from 'react'

function Map({ options, onMount, className, onMountProps }) {
  const ref = useRef()
  const [map, setMap] = useState()

  useEffect(() => {
    // The Map constructor modifies its options object in place by adding
    // a mapTypeId with default value 'roadmap'. This confuses shouldNotUpdate.
    // { ...options } prevents this by passing in a copy.
    const onLoad = () =>
      setMap(new window.google.maps.Map(ref.current, { ...options }))
    if (!window.google) {
      const script = document.createElement(`script`)
      script.src = `https://maps.googleapis.com/maps/api/js?key=` + YOUR_API_KEY
      document.head.append(script)
      script.addEventListener(`load`, onLoad)
      return () => script.removeEventListener(`load`, onLoad)
    } else onLoad()
  }, [options])

  if (map && typeof onMount === `function`) onMount(map, onMountProps)

  return (
    <div
      style={{ height: `60vh`, margin: ` 1em 0`, borderRadius: ` 0.5em` }}
      {...{ ref, className }}
    />
  )
}

function shouldNotUpdate(props, nextProps) {
  const [funcs, nextFuncs] = [functions(props), functions(nextProps)]
  const noPropChange = isEqual(omit(props, funcs), omit(nextProps, nextFuncs))
  const noFuncChange =
    funcs.length === nextFuncs.length &&
    funcs.every(fn => props[fn].toString() === nextProps[fn].toString())
  return noPropChange && noFuncChange
}

export default React.memo(Map, shouldNotUpdate)

Map.defaultProps = {
  options: {
    center: { lat: 48, lng: 8 },
    zoom: 5,
  },
}

Old Answer

Using html.js

Modifying src/html.js like so (as Nenu suggests) is one option.

import React, { Component } from 'react'
import PropTypes from 'prop-types'

export default class HTML extends Component {
  render() {
    return (
      <html {...this.props.htmlAttributes}>
        <head>
          <meta charSet="utf-8" />
          <meta httpEquiv="x-ua-compatible" content="ie=edge" />
          <meta
            name="viewport"
            content="width=device-width, initial-scale=1, shrink-to-fit=no"
          />
          {this.props.headComponents}
        </head>
        <body {...this.props.bodyAttributes}>
          {this.props.preBodyComponents}
          <div
            key={`body`}
            id="___gatsby"
            dangerouslySetInnerHTML={{ __html: this.props.body }}
          />
          {this.props.postBodyComponents}
          // MODIFICATION // ===================
          <script
            src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY"
            async
            defer
          />
          // ===================
        </body>
      </html>
    )
  }
}

HTML.propTypes = {
  htmlAttributes: PropTypes.object,
  headComponents: PropTypes.array,
  bodyAttributes: PropTypes.object,
  preBodyComponents: PropTypes.array,
  body: PropTypes.string,
  postBodyComponents: PropTypes.array,
}

Then you can access the Google Maps API anywhere in your project from window.google.maps.(Map|Marker|etc.).

The React way

To me that felt a little anachronistic, though. If you want a reusable React component that you can import into any page or template as import Map from './Map', I suggest this instead. (Hint: See update below for equivalent function component.)

// src/components/Map.js
import React, { Component } from 'react'

export default class Map extends Component {
  onLoad = () => {
    const map = new window.google.maps.Map(
      document.getElementById(this.props.id),
      this.props.options
    )
    this.props.onMount(map)
  }

  componentDidMount() {
    if (!window.google) {
      const script = document.createElement('script')
      script.type = 'text/javascript'
      script.src = `https://maps.google.com/maps/api/js?key=YOUR_API_KEY`
      const headScript = document.getElementsByTagName('script')[0]
      headScript.parentNode.insertBefore(script, headScript)
      script.addEventListener('load', () => {
        this.onLoad()
      })
    } else {
      this.onLoad()
    }
  }

  render() {
    return <div style={{ height: `50vh` }} id={this.props.id} />
  }
}

Use it like so:

// src/pages/contact.js
import React from 'react'

import Map from '../components/Map'

const center = { lat: 50, lng: 10 }
const mapProps = {
  options: {
    center,
    zoom: 8,
  },
  onMount: map => {
    new window.google.maps.Marker({
      position: center,
      map,
      title: 'Europe',
    })
  },
}

export default function Contact() {
  return (
    <>
      <h1>Contact</h1>
      <Map id="contactMap" {...mapProps} />
    </>
  )
}