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>
);
}
Related
I wanna update the component and change the lines.
When I try to send new props with coordinates, old coordinates also staying.
const PolylineDecorator = withLeaflet(props => {
const { positions } = props;
const polyRef = useRef();
useEffect(() => {
const polyline = polyRef.current.leafletElement; //get native Leaflet polyline
const { map } = polyRef.current.props.leaflet; //get native Leaflet map
L.polylineDecorator(polyline, {
patterns: props.patterns,
}).addTo(map);
}, []);
return <Polyline ref={polyRef} {...props} opacity="0" />;
});
It is tricky to debug this one out without trying in the local machine. But It looks like you need to pass positions to the useEffect as follows, so it will re-render every time positions are changing.
useEffect(() => {
const polyline = polyRef.current.leafletElement; //get native Leaflet
polyline
const { map } = polyRef.current.props.leaflet; //get native Leaflet map
L.polylineDecorator(polyline, {
patterns: props.patterns,
}).addTo(map);
}, [positions]);
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]);
Im new to react and building an app to better understand it. Im currently using the Google maps api to render a map and want to render multiple markers to the map.
Im having to use a geocode api to get the lat and longitude for each of my locations so I can display them on the map as markers. Currently my code isn't displaying anything to the screen and I think its because the map is trying to render the markers with an empty state variable before the fetch calls are completed.
I have tried to search for an answer but cant find anything that is similar to what Im trying to do.
Here is my code
import React, { useContext, useEffect, useState } from 'react';
import { GoogleMap, useLoadScript, Marker } from "#react-google-maps/api"
import Settings from "../Settings"
import mapSyles from "./MapStyles"
import { LogContext } from '../divelog/DiveLogProvider';
export const MapRender =(props) => {
const {diveLogs} = useContext(LogContext)
const [latLong, setLatLong] = useState()
//get the logs location and run that through api to get lat and long
useEffect(()=>{
let latLongs = []
diveLogs.map(dl =>{
return fetch(`http://api.positionstack.com/v1/forward?access_key=MYKEY&query=${dl.location}&limit=1
`)
.then(res => res.json())
.then(parsedRes => {
latLongs.push(parsedRes.data[0])
})
})
setLatLong(latLongs)
console.log(latLong)
},[diveLogs])
//here I want to map through the state variable and display all of the markers on the map
return (
<div>
<GoogleMap
mapContainerStyle={mapContainerStyle}
options={options}
zoom={1}
center={center}
>
{
//this is where I map through the state variable
latLong.map(l =>(
<Marker key={l.lat}
position ={{lat: l.latitude, lng: l.longitude}}
/>
))
}
</GoogleMap>
</div>
)
}
you can update the state right inside your response . like this
diveLogs.map(dl =>{
return fetch(`http://api.positionstack.com/v1/forward?access_key=MYKEY&query=${dl.location}&limit=1
`)
.then(res => res.json())
.then(parsedRes => {
setLatLong( prevLatLongs => [...prevLatLongs ,parsedRes.data[0]] )
})
})
Yo need update the state when the fetch result is ready:
useEffect(()=>{
diveLogs.map(dl =>{
return fetch(`http://api.positionstack.com/v1/forward?access_key=MYKEY&query=${dl.location}&limit=1
`)
.then(res => res.json())
.then(parsedRes => {
setLatLong((prev)=> [...prev, parsedRes.data[0]])
})
})
},[diveLogs])
I am using leaflet.locatecontrol to get users' location in the map but _onLocationFound doesn't work.
here is my map component
<Map
ref={ref}
center={initialState ? initialState.center : DEFAULT_CENTER}
zoom={initialState ? initialState.zoom : 5}
maxZoom={18}
onMove={(e) => {
onMapMove(e);
}}
>
<TileLayer
url={`${settings.getConfig().MAP_TILE_URL}${
settings.getConfig().MAP_X_API_KEY
}`}
attribution={settings.getConfig().MAP_ATTRIBUTION}
/>
{marker && (
<Marker
position={marker}
icon={L.icon({
iconUrl: "https://webstockreview.net/images/map-icon-png-6.png",
iconSize: [25, 30], // size of the icon
iconAnchor: [13, 36], // point of the icon which will correspond to marker's location
})}
/>
)}
<LocateControl options={locateOptions} />
</Map>
and this is the options I'm passing to LocateControl
position: "bottomright",
icon: "map-location-icon",
strings: {
title: "my location",
},
onLocationError: function (e) {
console.log(e);
console.log("Location access denied.");
},
_onLocationFound: function (e) {
console.log("event", e);
},
onActivate: () => {}, // callback before engine starts retrieving locations
}
and LocateControl is a simple component I implemented to separate the logic and here is its code:
import { withLeaflet } from "react-leaflet";
import Locate from "leaflet.locatecontrol";
class LocateControl extends Component {
componentDidMount() {
const { options, startDirectly } = this.props;
const { map } = this.props.leaflet;
const lc = new Locate(options);
lc.addTo(map);
if (startDirectly) {
// request location update and set location
lc.start();
}
}
render() {
return null;
}
}
export default withLeaflet(LocateControl);
my problem is that I want to save the location when it's found but _onLocationFound function won't be triggered even though the location is updated in the map and I can't find out what am I missing out because even in the documentation the function is simply past to locateControl
Here is the link to the source code of the package
I would be thankful if anyone could give me a hint
You're passing an option named _onLocationFound which is not on the list of supported options for Leaflet.LocateControl.
You probably did so because you looked at the code for Leaflet.LocateControl and confused the onLocationError option with the _onLocationError method, without noticing that the _onLocationError method explicitly calls its namesake option here...
_onLocationError: function(err) {
/* snip */
this.options.onLocationError(err, this);
},
...but you failed to realize that there is no onLocationFound option at all, and that the internal _onLocationFound method does not call any user-provided callback at all.
Instead, do it the documented way: Leverage the locationfound event on the map.
I am wondering if it is possible to render a react component within a mapboxgl.Popup(). Something like this:
componentDidMount() {
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(`<div>${<MapPopup />}<p>${moreText}</p></div>`)
//.setDOMContent(`${<MapPopup />}`) ?????
.addTo(this.props.mapboxMap)
})
Or should this be done using ReactDOM.render?
ReactDOM.render(<MapPopup />, document.getElementById('root'))
This project will have buttons and inputs in the popup that connect to a redux store.
Thanks for any input!
This works:
addPopup(el: JSX.Element, lat: number, lng: number) {
const placeholder = document.createElement('div');
ReactDOM.render(el, placeholder);
const marker = new MapboxGl.Popup()
.setDOMContent(placeholder)
.setLngLat({lng: lng, lat: lat})
.addTo(map);
}
(Where I've used typescript to illustrate types, but you can just leave these out for pure js.) Use it as
addPopup(<h1>Losers of 1966 World Cup</h1>, 52.5, 13.4);
You can try to implement React component:
export const Popup = ({ children, latitude, longitude, ...mapboxPopupProps }) => {
// this is a mapbox map instance, you can pass it via props
const { map } = useContext(MapboxContext);
const popupRef = useRef();
useEffect(() => {
const popup = new MapboxPopup(mapboxPopupProps)
.setLngLat([longitude, latitude])
.setDOMContent(popupRef.current)
.addTo(map);
return popup.remove;
}, [children, mapboxPopupProps, longitude, latitude]);
return (
/**
* This component has to have 2 divs.
* Because if you remove outter div, React has some difficulties
* with unmounting this component.
* Also `display: none` is solving that map does not jump when hovering
* ¯\_(ツ)_/¯
*/
<div style={{ display: 'none' }}>
<div ref={popupRef}>
{children}
</div>
</div>
);
};
After some testing, I have realized that Popup component was not rendering properly on the map. And also unmounting the component was unsuccessful. That is why there are two divs in return. However, it may happen only in my environment.
See https://docs.mapbox.com/mapbox-gl-js/api/#popup for additional mapboxPopupProps
useEffect dependencies make sure that MapboxPopup gets re-created every time something of that list changes & cleaning up the previous popup instance with return popup.remove;
I've been battling with this as well. One solution I found was using ReactDOM.render(). I created an empty popup then use the container generated by mapboxgl to render my React component.
marker.setPopup(new mapboxgl.Popup({ offset: 18 }).setHTML(''));
markerEl.addEventListener('mouseenter', () => {
markerEl.classList.add('enlarge');
if (!marker.getPopup().isOpen()) {
marker.getPopup().addTo(this.getMap());
ReactDOM.render(
component,
document.querySelector('.mapboxgl-popup-content')
);
}
});
const mapCardNode = document.createElement("div");
mapCardNode.className = "css-class-name";
ReactDOM.render(
<YourReactPopupComponent / > ,
mapCardNode
);
//if you have a popup then we remove it from the map
if (popupMarker.current) popupMarker.current.remove();
popupBox.current = new mapboxgl.Popup({
closeOnClick: false,
anchor: "center",
maxWidth: "240px",
})
.setLngLat(coordinates)
.setDOMContent(mapCardNode)
.addTo(map);
I used MapBox GL's map and popup events (to improve upon #Jan Dockal solution) which seemed to improve reliability. Also, removed the extra div wrapper.
import { useWorldMap as useMap } from 'hooks/useWorldMap'
import mapboxgl from 'mapbox-gl'
import { FC, useRef, useEffect } from 'react'
export const Popup: FC<{
layerId: string
}> = ({ layerId, children }) => {
const map = useMap() // Uses React Context to get a mapboxgl map (could possibly be null)
const containerRef = useRef<HTMLDivElement>(null)
const popupRef = useRef<mapboxgl.Popup>()
const handleClick = (
e: mapboxgl.MapMouseEvent & {
features?: mapboxgl.MapboxGeoJSONFeature[] | undefined
} & mapboxgl.EventData
) => {
// Bail early if there is no map or container
if (!map || !containerRef.current) {
return
}
// Remove the previous popup if it exists (useful to prevent multiple popups)
if (popupRef.current) {
popupRef.current.remove()
popupRef.current = undefined
}
// Create the popup and add it to the world map
const popup = new mapboxgl.Popup()
.setLngLat(e.lngLat) // could also use the coordinates from a feature geometry if the source is in geojson format
.setDOMContent(containerRef.current)
.addTo(map)
// Keep track of the current popup
popupRef.current = popup
// Remove the tracked popup with the popup is closed
popup.on('close', () => {
popupRef.current = undefined
})
}
useEffect(() => {
if (map && layerId) {
// Listen for clicks on the specified layer
map?.on('click', layerId, handleClick)
// Clean up the event listener
return () => {
map?.off('click', layerId, handleClick)
popupRef.current?.remove()
popupRef.current = undefined
}
}
}, [map, layerId])
return <div ref={containerRef}>{children}</div>
}
Try to do with onClick event, instead of creating a button. After that put your react component in onClick events add event listener refrence link
[1]: https://stackoverflow.com/a/64182029/15570982