How to get a ref to the dom node of a google map's marker in react? - reactjs

Using the google-maps-react npm package I can get a ref to the map's dom node like so:
loadMap() {
const maps = this.props.google.maps;
const mapRef = this.refs.map; <---- ref set in render function on dom node
const node = ReactDOM.findDOMNode(mapRef); <--- the dom node
...
this.map = new maps.Map(node, mapConfig);
this.mapRef = mapRef;
}
It's pretty easy because mapref is set in the render method:
render() {
return (
<div style={{ height: mapHeight }} ref="map">
...
And then that is used to set node and then that is used to new up the map.
How would I do this with a map's marker? A marker doesn't need a dom node to be created and therefore I cant get a ref to the marker.
this.marker = new google.maps.Marker({someOptions}); <----- no dom node needed
I want to do this because I need to dynamically change the icon of the marker based on some value in my redux store. I have tried changing the icon via props (see below), but it somehow prevents the icon marker from being draggable even though draggable is set to true.
return (
<Marker
key={foo}
position={latLngPos}
icon={ active ? activeIcon : defaultIcon }
draggable={true}
onDragstart={() => { return this.handleMapMarkerDragStart();}}
onDragend={() => { return this.handleMapMarkerDrop();}}
/>);
I suspect things are acting strangely because to get google's maps api to work with react, the components have to deal with the actual dom nodes instead of the virtual dom nodes.
Any insight into this would be much appreciated.

Regarding
I want to do this because I need to dynamically change the icon of the
marker based on some value in my redux store. I have tried changing
the icon via props (see below), but it somehow prevents the icon
marker from being draggable even though draggable is set to true.
the following example demonstrates how to:
set marker as draggable
set custom icon per marker
Example
const handleDragEnd = (props, marker, event) => {
console.log(event.latLng);
};
const defaultIcon =
"http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
const activeIcon =
"http://maps.google.com/mapfiles/kml/pushpin/pink-pushpin.png";
const MapWrapper = props => {
return (
<div className="map-container">
<Map
google={props.google}
className={"map"}
zoom={4}
initialCenter={{ lat: -24.9923319, lng: 135.2252427 }}
>
{props.places.map((place, i) => {
const active = i % 2 === 0;
return (
<Marker
icon={active ? activeIcon : defaultIcon}
key={place.id}
position={{ lat: place.lat, lng: place.lng }}
draggable={true}
onDragend={handleDragEnd}
/>
);
})}
</Map>
</div>
);
};
Note: Marker component does not support onDragstart event
listener at the moment in google-maps-react library, but it could be
attached directly to Google Maps Marker object
Demo

Related

How can I stop my google map reloading every time I try to do something else off the map?

I have React Page. I splitted the page in 2. In the first half I have a form with some fields, and In the second half I have a Google Map Component. I added a Polyline. The path of the polyline gets updated every time the user left clicks (add point) or right clicks (remove point), or if it's dragged. Everything works well until I leave the map and focus on inputs or press some buttons in the other half. Then if I try to add new points or remove them ( this is not working because it has no points to remove) it starts a new polyline.
My methods are very simple, I took them from the docs and they do the job.
const addLatLng = (event) => {
const path = poly.getPath();
path.push(event.latLng);
}
const removeVertex = (vertex) => {
let path = poly.getPath();
path.removeAt(vertex)
}
// eslint-disable-next-line no-undef
google.maps.event.addListener(poly, 'rightclick', function (event) {
if (event.vertex === undefined) {
return;
} else {
removeVertex(event.vertex)
}
})
<div style={{ height: '100vh' }}>
<GoogleMap
center={center}
zoom={3}
onLoad={(map) => setMap(map)}
mapContainerStyle={{ width: '100%', height: '100%' }}
onClick={(event) => { !showInputForLink && addLatLng(event) }}
>
</GoogleMap>
</div>
First methods do the action and this is how I declared the map.
const { isLoaded } = useJsApiLoader({
googleMapsApiKey: ct.MAPS_API_KEY,
libraries: ['drawing, places']
})
// eslint-disable-next-line no-undef
const [map, setMap] = useState(/** #type google.maps.Map */(null))
const poly = new google.maps.Polyline({
strokeColor: '#160BB9',
strokeOpacity: 1.0,
strokeWeight: 3,
editable: true,
});
poly.setMap(map)
And this is how I declared the poly.
I tried to look up in Docs to see if I missed something, but I couldn't find anything that will not lose the focus on the map when I do something else or to start another polyline.
I am not using different components, everything is in on file.
Should I declare a Poly component inside the map? and not use the traditional JavaScript method?
How can I create this without resetting the map when I do actions in the first half?

Rendering a MapContainer on several subpages results in error

I'm having some issues with MapContainer from react-leaflet throwing errors at me in my app.
I have a tabel of events, that links to a page with event details, this details page implements a map with the following component:
export function MapWithPoints(props: Props) {
const { points } = props
return (
<MapContainer scrollWheelZoom={false} style={{ height: 300, width: '100vw' }}>
<InnerMap points={points} />
</MapContainer>
)
}
InnerMap is defined as:
function InnerMap(props: Props) {
const { points } = props
const leafletMap = useMap()
React.useEffect(() => {
if (leafletMap && points.length) {
const pointArray: L.LatLngTuple[] = points.map((item) => [
item.lat,
item.lng,
])
const bounds = L.latLngBounds(pointArray)
leafletMap.fitBounds(bounds)
}
}, [points, leafletMap])
return (
<React.Fragment>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{points
.filter((poi) => poi.lat && poi.lng)
.map((poi) => (
<Marker
position={[poi.lat, poi.lng]}
key={`${poi.id}_${poi.lat}_${poi.lng}`}
icon={poi.icon ? poi.icon : DefaultIcon}
>
{poi.description ? <Popup>{poi.description}</Popup> : null}
</Marker>
))}
</React.Fragment>
)
}
The map works when I go to the first (random) entry in my list of events, the event page loads like it should, but if I go back and then choose another event page, MapContainer throws an error:
Uncaught Error: Map container is already initialized.
at NewClass._initContainer (Map.js:1092:1)
at NewClass.initialize (Map.js:136:1)
at new NewClass (Class.js:24:1)
at MapContainer.js:31:1
at commitAttachRef (react-dom.development.js:23645:1)
at safelyAttachRef (react-dom.development.js:22891:1)
at reappearLayoutEffectsOnFiber (react-dom.development.js:23545:1)
at reappearLayoutEffects_complete (react-dom.development.js:24838:1)
at reappearLayoutEffects_begin (react-dom.development.js:24826:1)
at commitLayoutEffects_begin (react-dom.development.js:24649:1)
I'm using react 18, and react-leaflet 4.0.0
Is there something I have missed in the setup?
There is a bug in 4.0.0, but even when using the latest version, I still get the error now and then. A workaround is to have a unique key on Map Container, for example timestamp. But this result in a memory leak, as each time a map is loaded it consumes more memory

Why is my React Native component not re-rendering on state update?

I'm struggling with this React-Native component for a few days now. You should probably know that React-Native is kind of new to me so... sorry if the solution is obvious to you.
I'm using react-native-maps and I have several markers on my map. Each one of them has some data stored in my state and I want the callout to display a piece of this state on press.
Here are my states :
const [markersDetails, setMarkersDetails] = useState([]);
const [activeMarker, setActiveMarker] = useState({});
My activeMarker is updated by this function :
const markerSearch = (markerId) => {
let stockMarker = markersDetails.find((singleMarker) => {
return Number(singleMarker.idMarker) === markerId;
});
console.log("Stock ", stockMarker);
setActiveMarker(stockMarker);
console.log("State ", activeMarker);
};
And this function is called, inside my return, with the onPress of any marker :
<Marker
key={Number(marker.index)}
coordinate={{
latitude: Number(marker.latitude),
longitude: Number(marker.longitude),
}}
pinColor="blue"
onPress={() => {
markerSearch(Number(marker.index));
}}
>
{activeMarker !== {} && activeMarker.markerName && (
<Callout>
<View>
<Text>{activeMarker.markerName}</Text>
</View>
</Callout>
)}
</Marker>
But whenever I press on a marker, the callout opens immediatly while my state is not yet updated. So the text within the callout refers either to the previous marker or is empty (if it's the first marker I press on).
I've checked with console.log and my state is clearly updated but it takes a little bit more time. And I don't know why my callout is not re-rendering when this state is updating.
I've tried a ton of things to make this works but I can't figure this out...
Try doing something like that:
You can extract the section to a new component
Then inside this use the useEffect hook
export default function CalloutComponent({activeMarker}) {
const [markerName, setMarkerName] = useState('')
useEffect(() => {
setMarkerName(activeMarker?.markerName)
}, [activeMarker?.markerName])
if(!!markerName) return null
return (
<Callout>
<View>
<Text>{markerName}</Text>
</View>
</Callout>
)
}
And use this new component in your Main view
<Marker
...
>
<CalloutComponent activeMarker={activeMarker}/>
</Marker>

How do I stop React Native Maps' Draggable Marker from disabling touches outside the MapView on iOS using Google as the provider?

On iOS with Google provider React Native Maps' Draggable Marker disables touches outside the MapView until it registers a touch inside the MapView. On Android everything is fine, but for some reason on iOS when I finish dragging a marker on the map and onDragEnd is called, no touch events are registered unless they are on TextInputs. Occasionally a TouchableOpacity will flash momentarily, but it's onPress function is never called. However if I touch inside the MapView, even nowhere near the marker, everything goes back to the way it's supposed to be. It's like react native maps has some finishing event that doesn't occur that forces the focus to stay on the map.
There are a couple tricky things going on but I don't think they're the culprits:
I use setState with onDragStart and onDragEnd to disable the ScrollView parent component, otherwise the dragging gets interrupted by the scroll on Android.
As part of onDragEnd I make a callout with react-native-geocoding, then update the region state. I have commented all this out and it still doesn't work.
The plan is to animate to the new region after the state is updated, but until this is resolved there's no point. Here's my code, or what's left of it after taking out all the commented stuff:
const MapSection = (props) => {
const {
location, setIsDraggingMarker, onDragMarkerEnd, region,
} = props;
const [isLoading, setIsLoading] = useState(false);
const onDragEnd = (e) => {
onDragMarkerEnd(e.nativeEvent.coordinate);
};
if (!isEmpty(location)) {
return (
<View style={styles.mapContainer}>
<MapView
provider={PROVIDER_GOOGLE}
style={styles.mapView}
initialRegion={region}
scrollEnabled={false}
zoomEnabled={false}
rotateEnabled={false}
>
<Marker
draggable
onDragStart={() => setIsDraggingMarker(true)}
onDragEnd={onDragEnd}
coordinate={region}
>
<Image
style={styles.imageStyle}
resizeMode="stretch"
source={require('../../../../assets/images/draggableMarkerPin.png')}
/>
<Callout
style={styles.customCallout}
onPress={() => { }}
>
<View style={styles.callout}>
<Feather
name="move"
size={12}
color="black"
/>
<Text style={{
paddingLeft: 4,
fontSize: 14,
}}
>
Press and drag to fine tune location!
</Text>
</View>
</Callout>
</Marker>
</MapView>
</View>
);
}
return null;
};
export default MapSection;
//FROM THE PARENT COMPONENT, PASSED AS PROPS
const onDragMarkerEnd = (coords) => {
setIsDraggingMarker(false);
setRegionWithLatLng(coords.latitude, coords.longitude);
};
const setRegionWithLatLng = async (latitude, longitude) => {
const fullLocationData = await geocodeLocationByCoords(latitude, longitude);
const currentLocation = { ...fullLocationData };
const newRegion = {
...region,
latitude: fullLocationData.geometry.location.lat,
longitude: fullLocationData.geometry.location.lng,
};
setRegion(newRegion);
dispatchEventDetailsState({
type: FORM_INPUT_UPDATE, value: currentLocation, isValid: true, input: 'location',
});
};
If there's any way to just trick the MapView into thinking it's been pressed (I'ved tried referencing the MapView and calling mapView.current.props.onPress(), no dice), then I'm fine with that. Just any workaround.

Markers not working with google-map-react

I'm working with the 'google-map-react' library and I have tried all but the markers are not showing up.
I pass the coords to the marker in many ways but none worked.
Here's my code & repository:
https://github.com/jorginyu/ubica
import React, { Component } from 'react';
import GoogleMapReact from 'google-map-react';
const API_KEY = 'WTFULOOKINAT';
const contacts = [
{ name: 'Spiderman', lat: 41.529616, lng: 2.434130 },
{ name: 'Iron Man', lat: 41.528103, lng: 2.433834 },
{ name: 'Hulk', lat: 41.530192, lng: 2.422994 }
];
const MarkersC = (text ) => <div className="contact">{text}</div>;
export default class MapComponent extends Component {
constructor(props) {
super(props);
this.state = {
center: {
lat: 41.528452,
lng: 2.434195
},
zoom: 18
}
}
render() {
return (
// Important! Always set the container height explicitly
<div className="mt-5" style={{ height: '80vh', width: '100%' }}>
<GoogleMapReact
bootstrapURLKeys={{ key: API_KEY }}
defaultCenter={this.state.center}
defaultZoom={this.state.zoom}
>
{contacts.map((contact,i) => {
<MarkersC position={{lat: contact.lat, lng: contact.lng}} text={contact.name} key={i} />
})}
</GoogleMapReact>
</div>
);
}
}
What can I do? Thanks for your time :)
There is a formatting problem. I deleted the spaces:
THEN:
{
contacts.map((contact, i) =>
<MarkersC lat={contact.lat} lng={contact.lng} text={contact.name} key={i} />
)
}
NOW:
{
contacts.map((contact, i) => <MarkersC lat={contact.lat} lng={contact.lng} text={contact.name} key={i} /> )
}
If you open the browser's console, you will see an error. The problem is with your MarkerC component and how you try to get the text prop.
The parameter of the component is an object with all properties that are passed to it.
You do not destructure it to get the text you simply use the whole parameter and try to display it.
So you need to propertly destructure it as const MarkersC = ( {text} ) => ..
Instead of
const MarkersC = ( text ) => <div className="contact">{text}</div>;
it should be
const MarkersC = ( {text} ) => <div className="contact">{text}</div>;
Update
Just noticed, the google-map-react expect to find lat and lng properties on the marker. You have wrapped them inside a position property so they cannot be found.
So your usage should be
either
<MarkersC lat={contact.lat} lng={contact.lng} text={contact.name} key={i} />
or spread the whole contact object that holds those properties
<MarkersC {...contact} key={i} />
so that the lat,lng and text are all direct properties of the MarkersC component.
Declaring the object type and referencing directly the value of 'text' on the initial 'const' worked for me:
const AnyReactComponent = (text:any) => <div>{text.text}</div>;
In your question, you have provided invalid GoogleMap key so it is showing error in console Google Maps JavaScript API error: InvalidKeyMapError.
So provide a valid GoogleMap key or blank(const API_KEY = '') for development use only.
In my case with blank API_KEY, it's working fine.
(Your git repo code is different than code you have posted here in StackOverflow.)

Resources