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:
useEffect(() => setMap(mapMaker(), [])
will set the value of state to the return value ofmapMaker
i.e an instance of google maps.- 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