JR
Use OS setting

Pages

No results for 'undefined'

Blog Posts

No results for 'undefined'
Powered by Algolia

Google Maps + React Hooks

Mar 19, 20192 min readWeb Dev, Tutorial, JS

Contents

Had to share this one since it’s so nice and simple. If you’re looking for a drop-in, zero-dependency Google Maps React component, look no further.

src/components/map.js
import React, { useEffect, useRef } from 'react'

export default function Map({ options, onMount, className }) {
  const divProps = { ref: useRef(), className }

  useEffect(() => {
    const onLoad = () => {
      const map = new window.google.maps.Map(divProps.ref.current, options)
      onMount && onMount(map)
    }
    if (!window.google) {
      const script = document.createElement(`script`)
      script.type = `text/javascript`
      script.src =
        `https://maps.googleapis.com/maps/api/js?key=` +
        process.env.GOOGLE_MAPS_API_KEY
      const headScript = document.getElementsByTagName(`script`)[0]
      headScript.parentNode.insertBefore(script, headScript)
      script.addEventListener(`load`, onLoad)
      return () => script.removeEventListener(`load`, onLoad)
    } else onLoad()
  }, [divProps.ref, onMount, options])

  return (
    <div
      css="height: 70vh; margin: 1em 0; border-radius: 0.5em;"
      {...divProps}
    />
  )
}

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

To use it, simply grab a free Google Maps API key from Google’s cloud console (here’s a guide for that) and either add it to your .env file or paste it in directly for process.env.GOOGLE_MAPS_API_KEY.

Then simply drop in the above Map component wherever you’d like to display a Google map.

src/app.js
import React from 'react'
import Map from './components/map.js'

export default () => (
  <div>
    <h1>Google Maps</h1>
    <Map />
  </div>
)

Customization

To change the area shown by the map and its zoom level, pass an options object containing the keys center and zoom.

mapProps = {
  options: {
    center: { lat: 20, lng: 40 },
    zoom: 4,
  },
}

<Map {...mapProps} />

If you’d like to do something more fancy, for instance add some markers to the map, you can also pass an onMount function:

const addMarkers = links => map => {
  links.forEach((link, index) => {
    const marker = new window.google.maps.Marker({
      map,
      position: link.coords,
      label: `${index + 1}`,
      title: link.title,
    })
    marker.addListener(`click`, () => {
      window.location.href = link.url
    })
  })
}

mapProps = {
  options: { center: { lat: 20, lng: 40 }, zoom: 4 },
  onMount: addMarkers(linksComingFromSomewhere),
}

<Map {...mapProps} />

link.coords should be an object of the same structure as center, i.e. with lat and lng keys for the latitude and longitude at which to display each marker.

Note that the onMount function must be curried since the Map component itself passes in the map object on which to apply onMount.

Optimization

By default, the Map component will rerender whenever the parent component rerenders. There are two problems with this. First, it wastes computation since there’s no need to rerender the map if its props didn’t change. Second and even more importantly, it ruins the user experience since the map will jump back to its initial center and zoom on every rerender. To prevent this, you can easily create a memoized map with the useCallback hook:

src/app.js
import React, { useCallback } from 'react'
import Map from './components/map.js'

const MemoMap = useCallback(<Map />, [])

export default () => (
  <div>
    <h1>This is a memoized map</h1>
    {MemoMap}
  </div>
)

In fact, you may want to make memoization part of the Map component itself by replacing

export default function Map({ options, onMount, className }) {
  ...
}

with

src/components/map.js
import { isEqual, omit, functions } from 'lodash'

function Map({ options, onMount, className }) {
  ...
}

const shouldUpdate = (prevProps, nextProps) => {
  const [prevFuncs, nextFuncs] = [functions(prevProps), functions(nextProps)]
  return (
    isEqual(omit(prevProps, prevFuncs), omit(nextProps, nextFuncs)) &&
    prevFuncs.every(fn => prevProps[fn].toString() === nextProps[fn].toString())
  )
}

export default React.memo(Map, shouldUpdate)

React.memo shallowly compares props and only rerenders a function component if the comparison returns false. It’s the equivalent of PureComponent for class components. For components that receive objects, arrays and functions as props which are often referentially different on every render, the default behavior of shallow prop comparison can be overridden by passing a custom comparison function as second argument. It takes the next and previous props as input and returns true if the update should be skipped or false if the component should rerender.

The above shouldUpdate function uses the functions and omit utilities imported from lodash to first identify and remove all (top-level) functions from prevProps and nextProps (in the above example, this only handles the onMount function but you may use additional functions in the future that would automatically be handled correctly by shouldUpdate). It then deep-compares the remaining props using isEqual followed by comparing the string representations of all omitted functions. If both comparisons return true, it skips the rerender and the user gets to keep the map’s current position and zoom level.

Final Implementation

Putting all of the above together, here’s the full component that I use in production.

src/components/map.js
import { functions, isEqual, omit } from 'lodash'
import React, { useEffect, useRef } from 'react'

function Map({ options, onMount, className }) {
  const divProps = { ref: useRef(), className }

  useEffect(() => {
    const onLoad = () => {
      const map = new window.google.maps.Map(divProps.ref.current, options)
      onMount && onMount(map)
    }
    if (!window.google) {
      const script = document.createElement(`script`)
      script.type = `text/javascript`
      script.src =
        `https://maps.googleapis.com/maps/api/js?key=` +
        process.env.GOOGLE_MAPS_API_KEY
      const headScript = document.getElementsByTagName(`script`)[0]
      headScript.parentNode.insertBefore(script, headScript)
      script.addEventListener(`load`, onLoad)
      return () => script.removeEventListener(`load`, onLoad)
    } else onLoad()
  }, [divProps.ref, onMount, options])

  return (
    <div
      css="height: 70vh; margin: 1em 0; border-radius: 0.5em;"
      {...divProps}
    />
  )
}

const shouldUpdate = (prevProps, nextProps) => {
  delete prevProps.options.mapTypeId
  const [prevFuncs, nextFuncs] = [functions(prevProps), functions(nextProps)]
  return (
    isEqual(omit(prevProps, prevFuncs), omit(nextProps, nextFuncs)) &&
    prevFuncs.every(fn => prevProps[fn].toString() === nextProps[fn].toString())
  )
}

export default React.memo(Map, shouldUpdate)

Map.defaultProps = {
  options: {
    center: { lat: 48, lng: 8 },
    zoom: 5,
  },
}
© 2019 - Janosh RiebesellThis site is open source
Powered byGatsbyGithubNetlify