Home > Mobile >  Google Maps Markers not showing using React and @googlemaps/js-api-loader
Google Maps Markers not showing using React and @googlemaps/js-api-loader

Time:09-17

I'm failing to add markers to my Google Maps component in a Gatsby project. I have been asked specifically not to use any third party libraries such as react-google-maps, so please no recommendations for those. I am to use @googlemaps/js-api-loader.

I can load the map just fine, but I can't get the marker to show up. It is being created as a new google.maps.Marker just fine, and logging the result shows that it's receiving the lng and lat correctly. I have tried all sorts of things (promises, callbacks, async/await, different scopes, etc.), but it seems that the markerMaker is completing prior to the mapMaker, even though I am attempting to use map as a dependency to the useEffect hook.

I have tried to store the map as a piece of state since it was returning a promise, but it's still only returning as a promise so I'm just confused by now.

Code below:

import React, { useState, useEffect } from "react"

import { Loader } from "@googlemaps/js-api-loader"

const loader = new Loader({
  apiKey: "APIKEY",
  version: "weekly",
  // libraries: ["places"],
})
const mapOptions = {
  center: {
    lat: -27.4705,
    lng: 153.026,
  },
  zoom: 12,
}

const mapMaker = async () => {
  try {
    const google = await loader.load()
    const map = await new google.maps.Map(
      document.getElementById("map"),
      mapOptions
    )
    console.log("map loaded")
    return map
  } catch (error) {
    console.log(error)
  }
}

const markerMaker = async (pos, map) => {
  try {
    const google = await loader.load()
    const marker = new google.maps.Marker({
      position: pos,
      setMap: map,
      title: "test",
    })
    console.log("marker loaded")
  } catch (error) {
    console.log(error)
  }
}

const GoogleMap = ({ className, practices }) => {
  const [map, setMap] = useState({})

  useEffect(() => {
    setMap(mapMaker())
  }, [])

  useEffect(() => {
    markerMaker({ lat: -27.081149410091783, lng: 152.9497979199946 }, map)
  }, [map])

  return <div className={className} id="map"></div>
}

export default GoogleMap

Any insights would be greatly appreciated.

CodePudding user response:

Given there is no working example I will assume that the issue is that the marker is being run before the Map is available.

There seem to be two incorrect assumptions you are running under:

  1. useEffect(() => setMap(mapMaker(), []) will set the value of state to the return value of mapMaker i.e an instance of google maps.
  2. The useEffect(fn, [maps]) will run only once the instance in the first point is declared.

In the first point what is actually happening is that the function mapMaker is async and thus returns a Pomise. Thus what your code is actually doing is setting the state to be a Promise {<pending>}. In order to fix this you will have to change your code to:

  useEffect(() => {
    mapMaker().then(map => setMap(map);
  }, [])

  // OR if you prefer async / await

  const loadMap = async () => {
    const map = await mapMaker();
    setMaps(map);
  }
  useEffect(() => {
    loadMap()
  }, [])

Now this alone won't fix the issue because of the second point. When you pass a dependency to useEffect it will run on initial value as well as every change of value.

So your code runs markerMaker and mapMaker effectively in parallel. Then runs markerMaker a second time when you set map to be Promise {<pending>} in the first useEffect.

In order to fix this you will need to check if map has been set to the instance of google maps before you run markerMaker.

A quick fix would be to do:

  const [map, setMap] = useState(null) // set initial state to null | undefined
  useEffect(() => {
    if (map !== null) { // check for the initial value
      markerMaker({ lat: -27.081149410091783, lng: 152.9497979199946 }, map)
    }
  }, [map])

You can play around with this in the sandbox.

CodePudding user response:

Here is the working solution I came up with. It's a bit verbose and not the prettiest, but it works. I am positive that it can be improved with refactoring, which I may come back and add if I have time.

The main issue was that the 'map' object wasn't existing globally. Rather than storing it in a piece of state, which I had trouble with doing before, I saved it as a variable.

You might also see that @MrUpsidown made a valid point about the setMap property. It does appear on the class, but I needed to use map instead. Totally open to critiquing this so that I may improve. Thanks in advance for anyone's input.

import React, { useState, useEffect, useRef } from "react"
import { Loader } from "@googlemaps/js-api-loader"

const loader = new Loader({
  apiKey: "key",
  version: "weekly",
  libraries: ["places"],
})
const mapOptions = {
  center: {
    lat: -27.4705,
    lng: 153.026,
  },
  zoom: 9,
}

const GoogleMap = ({ className, practices }) => {
  const [maps, setMaps] = useState(undefined)
  const mapRef = useRef(null)

  let map
  const mapMaker = mapRef => {
    map = new maps.Map(mapRef.current, mapOptions)
  }

  const markerMaker = (pos, map) => {
    const marker = new maps.Marker({
      position: pos,
      map: map,
      title: "test",
    })
  }

  const mapsObjectSetter = async () => {
    const google = await loader.load()
    setMaps(google.maps)
  }

  useEffect(() => {
    mapsObjectSetter()
  }, [])

  useEffect(() => {
    if (!maps) {
      return
    }
    mapMaker(mapRef)
  }, [maps])

  useEffect(() => {
    if (!maps) {
      return
    }
    practices.forEach(item => markerMaker(item.coords, map))
  }, [maps])

  return <div className={className} ref={mapRef}></div>
}

export default GoogleMap

  • Related