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...
Related
I am using lazy loading "loadModules " from esri-loader in order to load esri modules. The problem here is that I can't access the state to store the longitude and latitude values when 'search-complete' event fires.
I am also not able to override the "allPlaceholder" value when creating the Search widget
https://codesandbox.io/s/pedantic-hellman-sbcdz?
Any idea what I could be doing wrong ? is it possible to access the Search widget outside of componentDidMount ?
Thanks !
import React , { Component, Fragment } from 'react';
import { loadModules } from 'esri-loader';
import Col from 'react-bootstrap/Col';
import Row from 'react-bootstrap/Row';
class MapSearch extends Component {
constructor(props) {
super(props);
this.mapRef = React.createRef();
this.searchRef = React.createRef();
this.state = {
mysearch: null,
longitude: 0,
latitude: 0,
searchTerm: ""
};
}
componentDidMount() {
loadModules(['esri/Map', 'esri/views/MapView', 'esri/widgets/Search'], { css: true })
.then(([ArcGISMap, MapView, Search]) => {
const map = new ArcGISMap({
basemap: 'osm'
});
this.view = new MapView({
container: this.mapRef.current,
map: map,
center: [-85, 35],
zoom: 14
});
var mysearch = new Search({
view: this.view,
allPlaceholder: "TESTESTTEST", // this doesn't work
container: this.searchRef.current
});
mysearch.on("search-complete", function(event){
console.log(event);
console.log(this.state);
})
}
);
}
render() {
return (
<Fragment>
<Row >
<Col lg={7}><div className="arcmap" style={{"height": "50vh"}} ref={this.mapRef}></div></Col>
<Col lg={5}><div className="zobya-search" style={{ "wdith": "100%" }} ref={this.searchRef} ></div></Col>
</Row>
</Fragment>
);
}
}
export default MapSearch;
This turned out to be very simple, hopefully it will help someone else
just add a binding in the constructor such as
this.handleSearchComplete = this.handleSearchComplete.bind(this);
and create a new function
handleSearchComplete(event) {
this.setState({
longitude: event.results[0].results[0].feature.geometry.longitude ,
latitude: event.results[0].results[0].feature.geometry.latitude
});
}
then call this callback function such as
mysearch.on("search-complete", this.handleSearchComplete)
This turned out to be very simple, hopefully it will help someone else
just add a binding in the constructor such as
this.handleSearchComplete = this.handleSearchComplete.bind(this);
and create a new function
handleSearchComplete(event) {
this.setState({
longitude: event.results[0].results[0].feature.geometry.longitude ,
latitude: event.results[0].results[0].feature.geometry.latitude
});
}
then call this callback function such as
mysearch.on("search-complete", this.handleSearchComplete)
I am using mapbox-gl in a react app to render an interactive map. On map load, I want to center the map on the user's location while keeping the current level of zoom. Centering on the user's location works fine with geolocate.trigger(); but the map automatically zooms way out to the continent level. Here is a simplified version of the map component of the app. I start by setting a hard-coded center point in New York which works fine, and then when trigger() runs, it gets zoomed way out. I've tried fiddling with all the properties of the geolocate object, but none of them has any effect on the zoom that trigger() goes to, and trigger doesn't seem to take any arguments. Note that I've had to remove my mapboxgl.accessToken for security reasons.
As a side note, I'm also trying to get rid of the larger blue location confidence circle but I also can't seem to do that despite setting showAccuracyCircle: false. Any mapbox tips?
React component:
import React from 'react';
import mapboxgl from 'mapbox-gl';
import './map.css';
mapboxgl.accessToken = 'ACCESS_TOKEN_STRING_GOES_HERE';
export class Map extends React.Component {
constructor(props) {
super(props);
this.state = {
lng: -73.9392,
lat: 40.8053,
zoom: 17.5
};
}
componentDidMount() {
const map = new mapboxgl.Map({
container: this.mapContainer,
style: 'mapbox://styles/mapbox/streets-v11',
center: [this.state.lng, this.state.lat],
zoom: this.state.zoom
});
const geolocate = new mapboxgl.GeolocateControl({
container: this.geolocateContainer,
positionOptions: {enableHighAccuracy: true},
fitBoundsOptions: {linear: true, maxZoom: 10},
trackUserLocation: true,
mapboxgl: mapboxgl,
showAccuracyCircle: false
})
map.on('load', () => {
geolocate.trigger();
});
map.addControl(geolocate);
}
render() {
return (
<div>
<div ref={element => this.geolocateContainer = element} className='mapButtons' />
<div ref={el => this.mapContainer = el} className='mapContainer' />
</div>
)
}
}
Here is a code pen I put together that illustrates the issue if you insert your own mapboxgl.accessToken. If you look closely, right when the page loads up, a default view is shown at the coordinates and zoom level provided in this.state. But then it immediately centers on the user's location (this is the desired behavior) and zooms way out (I don't want to zoom out). You'll need to give the codepen page permission to access your location to see this.
https://codepen.io/lexandermorgan/pen/ZEWEzze
At the risk of pointing out the obvious, you should be setting minZoom, not maxZoom:
fitBoundsOptions: {linear: true, minZoom: map.getZoom()},
Also, if you're not actually using the geolocation tracking features, or need the control itself, it may be simpler to just use the browser's geolocation API directly:
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(e =>
map.jumpTo({ center: [e.coords.longitude, e.coords.latitude]}))
}
I found a slightly different work-around for this. It uses ReactMapGL instead of MapboxGL. The thing that seems to have made the difference is to pass a zoom parameter to a temporary geocoderDefaultOverrides object and then unpacking the viewport and then that object into the return object in handleGeocoderViewportChange. Here are the relevant parts:
handleGeocoderViewportChange = viewport => {
const geocoderDefaultOverrides = { transitionDuration: 0, zoom: 16 };
return this.handleViewportChange({
...viewport,
...geocoderDefaultOverrides
});
};
Then in render:
<Geocoder
mapRef={this.map_ref}
mapboxApiAccessToken={MBAccessToken}
onViewportChange={this.handleGeocoderViewportChange}
viewport={this.state.viewport}
/>
This question already has answers here:
How to implement Google Places API in React JS?
(2 answers)
Closed 2 years ago.
I want to use Google Maps and Places API in my React application for users to be able get hospitals in their area on the map.
I am using both #react-google-maps/api and use-places-autocomplete npm packages to interface with the API to display the map and places. I am using the useLoadScript hook to load in my API key and also declare libraries I will be using.
In this case, just as I have enabled Places and Maps JavaScript APIs, I supplied places in the libraries array.
I have tried adding the below script to the index.html file:
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places"></script>
but I get the error: You are loading the map multiple times.
Here's a snippet of my code where I use the useLoadScript hook from the #react-google-maps/api
Please how do I resolve this issue?
If you are using useLoadScript of the #react-google-maps/api library and use-places-autocomplete you need to supply the ['places'] as a value of your library to use the Places Autocomplete in your code. Please note that this useLoadScript will load the Google Maps JavaScript Script tag in your code and you don't need to add the script tag in the html file anymore.
From what I understand, your use case where the user will select a place from the autocomplete dropdown and the nearest hospital should be returned in the map.
To achieve this, you need to have this flow:
Use use-places-autocomplete to provide place suggestion when searching for a place. This library have a GetGeocode and a GetLatLng that you can use to get the coordinates of the chosen place.
Once you have the coordinates, you can use Places API Nearby Search, use the keyword hospital in your request and define a radius. This will search a list of hospital within the defined radius with the chosen place as the center.
You can loop through the list of result and pin each coordinates as markers in your map.
Here;s the sample code and a code snippet below. Make sure to add your API Key:
Index.js
import React from "react";
import { render } from "react-dom";
import Map from "./Map";
render(<Map />, document.getElementById("root"));
Map.js
/*global google*/
import React from "react";
import { GoogleMap, useLoadScript } from "#react-google-maps/api";
import Search from "./Search";
let service;
const libraries = ["places"];
const mapContainerStyle = {
height: "100vh",
width: "100vw"
};
const options = {
disableDefaultUI: true,
zoomControl: true
};
const center = {
lat: 43.6532,
lng: -79.3832
};
export default function App() {
const { isLoaded, loadError } = useLoadScript({
googleMapsApiKey: "YOUR_API_KEY",
libraries
});
const mapRef = React.useRef();
const onMapLoad = React.useCallback(map => {
mapRef.current = map;
}, []);
const panTo = React.useCallback(({ lat, lng }) => {
mapRef.current.panTo({ lat, lng });
mapRef.current.setZoom(12);
let map = mapRef.current;
let request = {
location: { lat, lng },
radius: "500",
type: ["hospital"]
};
service = new google.maps.places.PlacesService(mapRef.current);
service.nearbySearch(request, callback);
function callback(results, status) {
if (status === google.maps.places.PlacesServiceStatus.OK) {
for (let i = 0; i < results.length; i++) {
let place = results[i];
new google.maps.Marker({
position: place.geometry.location,
map
});
}
}
}
}, []);
return (
<div>
<Search panTo={panTo} />
<GoogleMap
id="map"
mapContainerStyle={mapContainerStyle}
zoom={8}
center={center}
options={options}
onLoad={onMapLoad}
/>
</div>
);
}
Search.js
import React from "react";
import usePlacesAutocomplete, {
getGeocode,
getLatLng
} from "use-places-autocomplete";
export default function Search({ panTo }) {
const {
ready,
value,
suggestions: { status, data },
setValue,
clearSuggestions
} = usePlacesAutocomplete({
requestOptions: {
/* Define search scope here */
},
debounce: 300
});
const handleInput = e => {
// Update the keyword of the input element
setValue(e.target.value);
};
const handleSelect = ({ description }) => () => {
// When user selects a place, we can replace the keyword without request data from API
// by setting the second parameter to "false"
setValue(description, false);
clearSuggestions();
// Get latitude and longitude via utility functions
getGeocode({ address: description })
.then(results => getLatLng(results[0]))
.then(({ lat, lng }) => {
panTo({ lat, lng });
})
.catch(error => {
console.log("😱 Error: ", error);
});
};
const renderSuggestions = () =>
data.map(suggestion => {
const {
place_id,
structured_formatting: { main_text, secondary_text }
} = suggestion;
return (
<li key={place_id} onClick={handleSelect(suggestion)}>
<strong>{main_text}</strong> <small>{secondary_text}</small>
</li>
);
});
return (
<div>
<input
value={value}
onChange={handleInput}
disabled={!ready}
placeholder="Where are you going?"
/>
{/* We can use the "status" to decide whether we should display the dropdown or not */}
{status === "OK" && <ul>{renderSuggestions()}</ul>}
</div>
);
}
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]
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