React google maps re-render issue using onBoundsChanged - javascript

I've got a project i'm working on where I use react google maps, however I've run into an issue where when I get the onBoundsChanged event and set state in the callback, it goes into a permanent loop of re rendering. I can only assume somehow that when the component re-renders after I call setState, it sets a new bounds and that will then re-trigger the callback and setState again, in form of an infinitely recursive loop.
import React from 'react'
import { compose, withProps, withStateHandlers, withState, withHandlers } from "recompose";
import {
withScriptjs,
withGoogleMap,
GoogleMap,
Marker
} from"react-google-maps";
import HouseDetails from './house/HouseDetails'
const { InfoBox } = require("react-google-maps/lib/components/addons/InfoBox");
class Map extends React.Component{
constructor(props){
super(props)
this.state = {
zoom: 15,
bounds: null
}
this.map = React.createRef()
this.onBoundsChanged = this.onBoundsChanged.bind(this)
this.onZoomChanged = this.onBoundsChanged.bind(this)
}
componentWillReceiveProps(test){
}
onBoundsChanged(){
this.setState({bounds: this.map.current.getBounds()}, ()=> console.log('update'))
let bounds = this.map.current.getBounds()
let realBounds = {lat:{west: bounds.ga.j, east: bounds.ga.l}, lon: {north: bounds.ma.j, south: bounds.ma.l}}
console.log(realBounds)
}
onZoomChanged(){
this.setState({zoom: this.map.current.getZoom()})
}
componentDidUpdate(){
console.log('hm')
}
render(){
return (
<GoogleMap
defaultZoom={15}
ref={this.map}
onZoomChanged={this.onZoomChanged}
onBoundsChanged={this.onBoundsChanged}
center={{ lat: 21.493468, lng: -3.177552}}
defaultCenter={this.props.center}>
</GoogleMap>
)
}
}
export default withScriptjs(withGoogleMap(Map))
The code for the component that re-renders to infinity is above, it doesn't bug out so as long as I don't setState in the onBoundsChanged function. Is there any way around this?

I am using react-google-maps.
if you want to update the marker on new position while dragging the map, you can use onBoundsChanged props
<GoogleMap
ref={refMap}
defaultZoom={13}
defaultCenter={{ lat: center.lat, lng: center.lng }}
onBoundsChanged={handleBoundsChanged}
fullscreenControl={false}
defaultOptions={defaultMapOptions}
onDragEnd={handleDragend}
>
<Marker position={center} />
</GoogleMap>
and in your handleBoundsChanged, you can update the center with new lat/lng
const handleBoundsChanged = () => {
console.log("handleBoundsChanged")
const lat = refMap.current.getCenter().lat()
const lng = refMap.current.getCenter().lng()
const mapCenter = {
lat: lat,
lng: lng,
}
setCenter(mapCenter); // move the marker to new location
};
if you want to move the map to new lat/lng programatically, you can use panTo function in useEffect when the address is updated. This is needed when you input the address in search and you want your map and marker at new location
//on address update
useEffect(() =>{
console.log("props updates", props.address, props.locationName)
if(props.address.source == 'searchbar'){
console.log("in the searchbar props")
const mapCenter = {
lat: props.address.latitude,
lng: props.address.longitude,
}
refMap.current.panTo(mapCenter) //move the map to new location
setCenter(mapCenter) // move the marker to new location
}
},[props.address])

I converted the class above from the docs compose examples, I must have screwed up somewhere because my mistake was very obvious, since on render the center is set for the GoogleMap object, all I had to do was have an initial state with a location, set the default state and then remove the 'center' prop from the component, this means that on re-render it does not pull it back to the center, and therefore does not re-trigger onBoundsChanged

Related

Google map API dynamic lat/lng initial center not showing correct location

I am using local storage to store a lat/lng that is part of a product from another component. I get item lat/lng and set them to state, to use in maps. When I step through everything it is the correct lat:32.735962/lng:-96.275256 for a location just outside Dallas TX, but when the map renders it is centered in the Gulf of Guinea. If I hard code the same lat/lng it renders the correct location.
import { Map, GoogleApiWrapper, Marker } from 'google-maps-react';
import '../css/googleMaps.css';
export class Maps extends React.Component {
constructor(props){
super(props);
this.state = {
lat: '',
lng: ''
};
}
componentDidMount(){
const lat = localStorage.getItem('lat')
const lng = localStorage.getItem('lng')
this.setState({
lat: lat,
lng: lng
})
}
logOut = () => {
localStorage.removeItem('token');
window.location = '/';
};
back = () => {
localStorage.removeItem('lat')
localStorage.removeItem('lng')
window.location = '/patronmain';
};
render() {
const mapStyles = {
width: "60%",
height: "60%",
};
return (
<div>
<p className="lOut" onClick={() => this.logOut()}>Logout</p>
<p className="lOut" onClick={() => this.back()}>Back</p>
<Map
google={this.props.google}
zoom={5}
style={mapStyles}
initialCenter={{lat: this.state.lat, lng: this.state.lng}}
>
<Marker
label='Made Here'
postion={{ lat: this.state.lat, lng: this.state.lng }}
/>
</Map>
</div>
);
}
}
export default GoogleApiWrapper({
apiKey: ''
})(Maps);
Your map is being centered somewhere in the Gulf of Guinea because the value of your initialCenter came from your state which has a value of "". This means that the value of your latLng is "","" which the map considered as 0,0 .
Please note that initialCenter parameter takes an object containing latitude and longitude coordinates. Sets the maps center upon loading.
To change the center of the map when changing the value of the state you need to use the center parameter which takes an object containing latitude and longitude coordinates. This is use if you want to re-render the map after the initial render.

How do I get to the updated path of an editable PolyLine from react-google-maps/api?

I'm using the react-google-maps/api library, and I have an application where I need the user to edit a Polyline.
The problem I'm having is grabbing the path of the polyline after the user has finished editing.
If I use native react components, the path returned on the props from the polyline is the original path of the line - not the one edited by the user.
The code below is a cutdown version of where I'm trying to get the path of the line from the react component. If you try it and edit the line, the return array is the original path. I've seen some examples using the getPath() method, but I just can't seem to get this to work on the React component (ie polylineRef.current.getPath() returns a no function error.
How should I be getting the path information of the edited line?
import React, { Fragment, useRef } from "react";
import { GoogleMap, Polyline, useLoadScript } from "#react-google-maps/api";
const MapTest = (props) => {
const polylineRef = useRef();
const mapRef = useRef();
const mapContainerStyle = {
width: "80vw",
height: "80vh",
};
const showPath = () => {
console.log(polylineRef.current.props.path); //What should be here to show the edited path if its possible to access?
};
const { isLoaded, loadError } = useLoadScript({
googleMapsApiKey: process.env.REACT_APP_GOOGLE_KEY,
});
const centre = { lat: 51.999889, lng: -0.98807 };
if (loadError) return "Error loading Google Map";
if (!isLoaded) return "Loading Maps....";
console.log(polylineRef.current.props.path);
return (
<Fragment>
<GoogleMap
mapContainerStyle={mapContainerStyle}
ref={mapRef}
zoom={10}
center={centre}
>
<Polyline
ref={polylineRef}
key={1}
path={[
{ lat: 51.9298274729133, lng: -1.0446431525421085 },
{ lat: 51.98483618577529, lng: -1.2423970587921085 },
]}
options={{ editable: true, strokeColor: "#ff0000" }}
/>
</GoogleMap>
<button
onClick={(event) => {
showPath(event);
}}
>
Show Path in Console
</button>
</Fragment>
);
};
export default MapTest;
If I use the native google API, then I can see the updated path, but I can't get a reference to the map created by the map to place the polyline onto.
If I can't access the edited path through the react component, how should I provide a reference to the google maps native API, so when I do
polyline = new google.maps.Polyline(//polyline options)
polyline.setMap(map) //Where do I get the handle for this map to put it on the map above?
/*I've tried using mapRef.current (not a map instance) and
mapRef.current.getInstance() - this makes the original map disappear, for reasons I don't understand*/
When I build this using the native API, I can access the edited path using the getPath() method, but I can't render this polyline on the component rendered above.
Other than building the map out of the native API I'm struggling to do this at the moment - but the benefits of the ease of rendering of React make me want to continue down this path for a while longer - is anyone able to help please?
I think this is what you are trying to achieve:
https://codesandbox.io/s/snowy-night-ony59?file=/src/App.js
My answer is based on:
https://codesandbox.io/s/reactgooglemapsapi-editing-a-polygon-popr2?file=/src/index.js:2601-2845
which I found by googling: react-google-maps-api editable polygon
Basically just copying and pasting the code referred by Daniele Cordano
import React, { useState, useRef, useCallback } from "react";
import ReactDOM from "react-dom";
import { LoadScript, GoogleMap, Polygon } from "#react-google-maps/api";
import "./styles.css";
// This example presents a way to handle editing a Polygon
// The objective is to get the new path on every editing event :
// - on dragging the whole Polygon
// - on moving one of the existing points (vertex)
// - on adding a new point by dragging an edge point (midway between two vertices)
// We achieve it by defining refs for the google maps API Polygon instances and listeners with `useRef`
// Then we bind those refs to the currents instances with the help of `onLoad`
// Then we get the new path value with the `onEdit` `useCallback` and pass it to `setPath`
// Finally we clean up the refs with `onUnmount`
function App() {
// Store Polygon path in state
const [path, setPath] = useState([
{ lat: 52.52549080781086, lng: 13.398118538856465 },
{ lat: 52.48578559055679, lng: 13.36653284549709 },
{ lat: 52.48871246221608, lng: 13.44618372440334 }
]);
// Define refs for Polygon instance and listeners
const polygonRef = useRef(null);
const listenersRef = useRef([]);
// Call setPath with new edited path
const onEdit = useCallback(() => {
if (polygonRef.current) {
const nextPath = polygonRef.current
.getPath()
.getArray()
.map(latLng => {
return { lat: latLng.lat(), lng: latLng.lng() };
});
setPath(nextPath);
}
}, [setPath]);
// Bind refs to current Polygon and listeners
const onLoad = useCallback(
polygon => {
polygonRef.current = polygon;
const path = polygon.getPath();
listenersRef.current.push(
path.addListener("set_at", onEdit),
path.addListener("insert_at", onEdit),
path.addListener("remove_at", onEdit)
);
},
[onEdit]
);
// Clean up refs
const onUnmount = useCallback(() => {
listenersRef.current.forEach(lis => lis.remove());
polygonRef.current = null;
}, []);
console.log("The path state is", path);
return (
<div className="App">
<LoadScript
id="script-loader"
googleMapsApiKey=""
language="en"
region="us"
>
<GoogleMap
mapContainerClassName="App-map"
center={{ lat: 52.52047739093263, lng: 13.36653284549709 }}
zoom={12}
version="weekly"
on
>
<Polygon
// Make the Polygon editable / draggable
editable
draggable
path={path}
// Event used when manipulating and adding points
onMouseUp={onEdit}
// Event used when dragging the whole Polygon
onDragEnd={onEdit}
onLoad={onLoad}
onUnmount={onUnmount}
/>
</GoogleMap>
</LoadScript>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
For some reason, the useRef hook doesn't accept the function getPath/getPaths() with the typescript error : TS2339: Property 'getPath' does not exist on type 'MutableRefObject'
const handleNewPolygonPath = useCallback(() => {
const newPath = polygonRef.getPath();
dispatch(setNewPolygonPath(newPath));
console.log(newPolygonPath);
}, [dispatch, newPolygonPath]);

How to Set Leaflet Map's Zoom to Show All Markers in React Leaflet?

I have a React Leaflet map that needs to change its center and zoom when given a set of markers. The zoom should be changed such that all the markers are visible.
This change of view is currently being attempted using the function ChangeView.
Using my code below, I am able to move the map view, but not able to let the map fit to the bounds. Running the code gives the error:
Error: Bounds are not valid.
on the line
map.fitBounds(markerBounds)
What can we do? Thanks!
import L, { LatLng, latLngBounds, FeatureGroup } from 'leaflet';
import React from 'react';
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
import MarkerClusterGroup from 'react-leaflet-markercluster';
import { LatLon, MapMarker } from '../../common/types';
import { config } from '../../config';
interface IProps {
markers: MapMarker[];
searchCenter: LatLon;
}
interface IChangeView {
center: LatLon;
markers: MapMarker[];
}
function ChangeView({ center, markers }: IChangeView) {
const map = useMap();
map.setView({lng: center.lon, lat: center.lat}, DEFAULT_ZOOM);
let markerBounds = latLngBounds([]);
markers.forEach(marker => {
markerBounds.extend([marker.lat, marker.lon])
})
map.fitBounds(markerBounds) // <===== Error: Bounds are not valid.
return null;
}
export function MapView({markers, searchCenter}: IProps): JSX.Element {
return (
<MapContainer
center={[searchCenter.lat, searchCenter.lon]}
zoom=14
style={{ width:'100%', height:'100vh' }}
className='markercluster-map'
>
<ChangeView center={searchCenter} markers={markers} />
<TileLayer
url={`https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}?access_token=${config.api.mapbox}`}
/>
<MarkerClusterGroup>
{
markers.map((marker, index) => (
<Marker
position={[marker.lat, marker.lon]}
key={index}
/>
))
}
</MarkerClusterGroup>
</MapContainer>
)
}
Also tried using FeatureGroup instead of latLngBounds, but it gave the exact same error
Error: Bounds are not valid
let group = new FeatureGroup();
markers.forEach(marker => {
L.marker([marker.lat, marker.lon]).addTo(group);
})
map.fitBounds(group.getBounds());
If the markers array is empty, or null, the bounds you create will not have ._southWest and ._northEast properties, and that error will throw. Just make the fitBounds statement conditional on there being markers in the array:
if (markers.length && markers.length > 0){
markers.forEach(marker => {
markerBounds.extend([marker.lat, marker.lon])
})
map.fitBounds(markerBounds)
}
Or even just a quick one liner:
markerBounds.isValid() && map.fitBounds(markerBounds)

ReactJS and Google Maps - Displaying Markers

I'm React newbie so maybe asking a stupid question, but this has got me perplexed. As part of my learning I'm building a three component application - a parent About, and two children (GoogleMap and MapMarkerDetails). The parent does the data coordination and one child shows a google map with two markers as default, the other child shows details of the markers when it is clicked.
I am now adding functionality to add a new marker when the map is clicked. Most of the functionality works - the maps is draw, the default markers are added, and when one of the markers is clicked, this calls a function on the parent class which updates its state and this is propagated to the MapMarkerDetails element and a simple message is displayed.
Here is the parent class which I have commented to help understanding:
import React, { Component } from 'react';
import GoogleMap from './GoogleMap'
import MapMarkerDetails from './MapMarkerDetails'
class About extends Component {
state = {
markers: [{
position: { lat: 51.438759, lng: -2.864514 },
label: 'A',
map: null,
}, {
position: { lat: 51.433636, lng: -2.868734 },
label: 'B',
map: null,
}],
greeting: 'HelloA'
}
showMarkerInfo = (label) => {
this.setState({greeting: ('Hello ' + label)})
}
/*
Adding a new Marker
This function is called from the child element GoogleMap
However, the setState(...) dosn't seem to propogate down to the GoogleMap element.
*/
addMarker = (latlng) => {
let newMarker = [{
position: { lat: latlng.lat(), lng: latlng.lng() },
label: 'New',
map: null,
}];
/* Creates a new array for updating state. Is this the best way to do this */
let markers = [...this.state.markers, ...newMarker]
this.setState({markers});
console.log(this.state) // This shows the added marker
}
render() {
return (
<div className="container">
<h4 className="center">About</h4>
<MapMarkerDetails details={this.state.greeting}/>
<GoogleMap markers={this.state.markers} clickedMarker={this.showMarkerInfo} addMarker={this.addMarker}/>
</div>
)
}
}
export default About;
Here is the class that displays Google Map and the markers:
import React, { Component } from 'react';
class GoogleMap extends Component {
constructor(props) {
super(props);
this.googleMapRef = React.createRef(); // Create a referance for Google Map to draw to
console.log('Constructore')
}
componentDidMount(){
console.log('componentDidMount')
/* Create the Map */
let googleMap = new window.google.maps.Map(this.googleMapRef.current, {
zoom: 15,
center: {
lat: 51.436411,
lng: -2.861980,
},
disableDefaultUI: true,
})
this.placeMMarkers(googleMap) // Place the markers
this.addMapListner(googleMap) // Define a click listener to place new markers
}
/* Place the markers */
placeMMarkers = (googleMap) => {
this.props.markers.forEach((m) => {
m.map = googleMap;
let marker= new window.google.maps.Marker(m)
marker.addListener('click', () => { this.props.clickedMarker(m.label); });
}
);
}
/* Map listeners */
addMapListner = (googleMap) => {
googleMap.addListener('click', (e) => {
this.props.addMarker(e.latLng)
})
}
render() {
console.log('render: ' + this.props.markers) // This is showing the added markers
return (
<div
id="google-map"
ref={this.googleMapRef}
style={{ width: '800px', height: '400px', float: 'left' }}>
</div>
)
}
}
export default GoogleMap
I've added console logging to each function so I can follow what is happening.
Here is the MapMarkerDetails which displays a simple message when an marker is clicked. This all works fine.
import React, { Component } from 'react';
class MapMarkerDetails extends Component {
render(){
return (
<div style={{width: '100px', height: '400px', backgroundColor: 'gray', float: 'left'}}>
{this.props.details}
</div>
)
}
}
export default MapMarkerDetails
Description of the Problem
When the user clicks on the map (not a marker) this invokes the function addMarker which is passed down from the parent About class (snippet below). In the addMarker function of About the lat/lng is passed in. This represents where the user clicked. This is converted into a marker data object, then a new array is created which contains the default markers and the new one. I'm not sure if my new array creation is done in the best way - if not let me know.
Once the new array is created, we update the components state with this.setState({markers}). I thought this would lead to a re-render() and an redrawing of the map with the added marker. But not.
addMarker = (latlng) => {
let newMarker = [{
position: { lat: latlng.lat(), lng: latlng.lng() },
label: 'New',
map: null,
}];
/* Creates a new array for updating state. Is this the best way to do this */
let markers = [...this.state.markers, ...newMarker]
this.setState({markers});
console.log(this.state) // This shows the added marker
}
Something happens that results in the render() function of GoogleMap being called, but only the original markers are shown. The data is passed down to the GoogleMap component because I can see the output of console.log('render: ' + this.props.markers). But how do I get ALL the markers to load?
Please advise on what is the best way to for About to pass data to GoogleMap such that it can add in the new marker.
Just like you use componentDidMount to imperatively add the markers when the map is first loaded, you should use componentDidUpdate to do the same thing when the props change. In your GoogleMap component:
componentDidUpdate() {
this.placeMMarkers()
}
Rather than passing googleMap as an argument, I would set it as an instance variable in componentDidMount:
this.googleMap = new window.google.maps.Map(...
and then change placeMMarkers to use this.googleMap:
placeMMarkers = () => {
this.props.markers.forEach((m) => {
m.map = this.googleMap;
// ...
Since you are attaching an event handler in placeMMarkers, you should also add some logic to distinguish between new markers and existing ones, to avoid adding multiple event handlers to existing markers.
In response to your question about how best to set the state, I think what you've got is fine but you don't need to put the new marker inside an array:
let newMarker = {
position: { lat: latlng.lat(), lng: latlng.lng() },
label: 'New',
map: null,
};
let markers = [...this.state.markers, newMarker]

Using Google Map API inside a React component

I am struggling over the way to use Google Map API inside a React component. I did not want to use popular react-google-maps nor google-map-react packages, but rather create my own.
I managed to load the script tag with the Google Map API from the React component. However, how do I manipulate the Google API from here? For example, initializing the map with even basic configuration like below?
map = new google.maps.Map(document.getElementById('map'), {
center: {lat: -34.397, lng: 150.644},
zoom: 8
});
Here is my component. Any advice is appreciated! thanks!
import React, { Component } from 'react';
// Load Google API in script tag and append
function loadScript(src) {
return new Promise((resolve, reject) => {
let script = document.createElement('script');
script.src = src;
script.addEventListener('load', function() {
resolve();
});
script.addEventListener('error', function(e) {
reject(e);
});
document.body.appendChild(script);
});
}
const script = 'https://maps.googleapis.com/maps/api/js?key=MY_API_KEY';
class MyGoogleMap extends Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
// first load the script into html
loadScript(script).then(function() {
console.log('SUCCESS');
// Where do I go from here?
});
}
render() {
return <div />;
}
}
export default MyGoogleMap;
I actually found my own solution, so I am sharing for anyone who would meet the same problem.
The basic logic is to use window object to access google.
So, after I load the script as I did from the question, I initialize my map as:
initMap = () => {
// 'google' could be accessed from 'window' object
const map = new window.google.maps.Map(
document.getElementById('googleMap'),
{
zoom: 14,
center: { lat: LATITUDE, lng: LONGTITUDE }
}
);
// putting a marker on it
const marker = new window.google.maps.Marker({
position: { lat: LATITUDE, lng: LONGTITUDE },
map: map
});
};
render() {
return (
<div id="googleMap" style={width: WIDTH, height: HEIGHT}/>
);
}
Any comment is welcomed :)
Create GoogleMap component with ref, to display google map inside that div.
import React, { Component } from 'react';
class GoogleMap extends Component {
componentDidMount() {
new google.maps.Map(this.refs.map, {
zoom: 12,
center: {
lat: this.props.lat,
lng: this.props.lon
}
});
}
render() {
return <div className="google-map" ref="map" />
}
}
export default GoogleMap;
Use it in Component you like like this:
<GoogleMap lat={lat} lon={lon}/>
by passing latitude and longitude of a city. To be able to see it you need to set width and height of css class google-map (or whatever you name it). for example:
div.google-map {
height: 150px;
width: 250px;
}
Fiddle.js preview
EDIT
inside head load script:
<script src="https://maps.googleapis.com/maps/api/js"></script>
I tought you have done that, because in your code you also use new google.maps.... If you cant call it like just google, try new window.google.maps...

Categories

Resources