Page Summary
-
This example demonstrates the integration of the Places Autocomplete widget within a React application to dynamically update a map and marker.
-
It leverages the
vis.gl/react-google-mapslibrary, providing React components for interacting with the Google Maps JavaScript API. -
The provided code snippets include TypeScript, JavaScript, CSS, and HTML to showcase the complete implementation.
-
Although the
vis.gl/react-google-mapslibrary is open source and not covered by Google Maps Platform support, the underlying Google Maps services used are still subject to the Google Maps Platform Terms of Service.
This example shows using the Places Autocomplete widget to update a map and marker in a React application. It uses the vis.gl/react-google-maps open source library. The vis.gl/react-google-maps library is a collection of React components and hooks for the Google Maps JavaScript API.
TypeScript
import React, { useState, useEffect, useRef } from 'react'; import { createRoot } from 'react-dom/client'; import { APIProvider, Map, MapControl, ControlPosition, AdvancedMarker, InfoWindow, useMap, useMapsLibrary, useAdvancedMarkerRef, } from '@vis.gl/react-google-maps'; const API_KEY = 'GOOGLE_MAPS_API_KEY'; declare global { namespace JSX { interface IntrinsicElements { 'gmp-place-autocomplete': React.DetailedHTMLProps< React.HTMLAttributes<HTMLElement>, HTMLElement >; } } } const PlaceAutocomplete = ({ onPlaceSelect, }: { onPlaceSelect: (place: google.maps.places.Place | null) => void; }) => { const map = useMap(); const placesLibrary = useMapsLibrary('places'); const containerRef = useRef<HTMLDivElement>(null); useEffect(() => { if (!map || !placesLibrary || !containerRef.current) return; // 1. Programmatically instantiate the modern PlaceAutocompleteElement const autocomplete = new placesLibrary.PlaceAutocompleteElement(); containerRef.current.appendChild(autocomplete); // 2. Manually sync the map's bounds to the autocomplete's locationRestriction. // We use map.getBounds().toJSON() to pass a plain object literal, which safely // bypasses any cross-context 'instanceof' wipeout issues in React. const syncBounds = () => { const bounds = map.getBounds(); if (bounds) { autocomplete.locationRestriction = bounds.toJSON(); } }; // Sync initially and whenever the map moves. syncBounds(); const boundsListener = map.addListener('bounds_changed', syncBounds); // 3. Listen for the gmp-select event. const placeSelectListener = (e: Event) => { const event = e as google.maps.places.PlacePredictionSelectEvent; const place = event.placePrediction.toPlace(); void place .fetchFields({ fields: [ 'location', 'viewport', 'displayName', 'formattedAddress', ], }) .then(() => { if (place.viewport) { map.fitBounds(place.viewport); } else if (place.location) { map.setCenter(place.location); map.setZoom(13); } onPlaceSelect(place); }) .catch((err: unknown) => { console.error(err); }); }; autocomplete.addEventListener('gmp-select', placeSelectListener); return () => { google.maps.event.removeListener(boundsListener); autocomplete.removeEventListener('gmp-select', placeSelectListener); // Clean up the DOM element when unmounting. if (containerRef.current) { containerRef.current.innerHTML = ''; } }; }, [map, placesLibrary, onPlaceSelect]); return ( <div className="place-autocomplete-card" style={{ backgroundColor: '#fff', borderRadius: '5px', boxShadow: 'rgba(0, 0, 0, 0.35) 0px 5px 15px', margin: '10px', padding: '5px', fontFamily: 'Roboto, sans-serif', fontSize: 'small', width: '300px', }}> <div ref={containerRef} style={{ width: '100%' }} /> </div> ); }; export default function App() { const [selectedPlace, setSelectedPlace] = useState<google.maps.places.Place | null>(null); const [markerRef, marker] = useAdvancedMarkerRef(); return ( <APIProvider apiKey={API_KEY}> <Map defaultCenter={{ lat: 40.749933, lng: -73.98633 }} defaultZoom={13} gestureHandling={'greedy'} mapId="DEMO_MAP_ID" disableDefaultUI={true}> <MapControl position={ControlPosition.BLOCK_START_INLINE_START}> <PlaceAutocomplete onPlaceSelect={setSelectedPlace} /> </MapControl> {selectedPlace?.location && ( <AdvancedMarker ref={markerRef} position={selectedPlace.location} /> )} {selectedPlace?.location && marker && ( <InfoWindow anchor={marker}> <div> <span style={{ fontWeight: 'bold' }}> {selectedPlace.displayName ?? 'No name'} </span> <br /> <span> {selectedPlace.formattedAddress ?? 'No address'} </span> </div> </InfoWindow> )} </Map> </APIProvider> ); } export function renderToDom(container: HTMLElement) { const root = createRoot(container); root.render( <React.StrictMode> <App /> </React.StrictMode> ); }
JavaScript
import React, { useState, useEffect, useRef } from 'react'; import { createRoot } from 'react-dom/client'; import { APIProvider, Map, MapControl, ControlPosition, AdvancedMarker, InfoWindow, useMap, useMapsLibrary, useAdvancedMarkerRef, } from '@vis.gl/react-google-maps'; const API_KEY = 'GOOGLE_MAPS_API_KEY'; const PlaceAutocomplete = ({ onPlaceSelect, }) => { const map = useMap(); const placesLibrary = useMapsLibrary('places'); const containerRef = useRef(null); useEffect(() => { if (!map || !placesLibrary || !containerRef.current) return; // 1. Programmatically instantiate the modern PlaceAutocompleteElement const autocomplete = new placesLibrary.PlaceAutocompleteElement(); containerRef.current.appendChild(autocomplete); // 2. Manually sync the map's bounds to the autocomplete's locationRestriction. // We use map.getBounds().toJSON() to pass a plain object literal, which safely // bypasses any cross-context 'instanceof' wipeout issues in React. const syncBounds = () => { const bounds = map.getBounds(); if (bounds) { autocomplete.locationRestriction = bounds.toJSON(); } }; // Sync initially and whenever the map moves. syncBounds(); const boundsListener = map.addListener('bounds_changed', syncBounds); // 3. Listen for the gmp-select event. const placeSelectListener = (e) => { const event = e; const place = event.placePrediction.toPlace(); void place .fetchFields({ fields: [ 'location', 'viewport', 'displayName', 'formattedAddress', ], }) .then(() => { if (place.viewport) { map.fitBounds(place.viewport); } else if (place.location) { map.setCenter(place.location); map.setZoom(13); } onPlaceSelect(place); }) .catch((err) => { console.error(err); }); }; autocomplete.addEventListener('gmp-select', placeSelectListener); return () => { google.maps.event.removeListener(boundsListener); autocomplete.removeEventListener('gmp-select', placeSelectListener); // Clean up the DOM element when unmounting. if (containerRef.current) { containerRef.current.innerHTML = ''; } }; }, [map, placesLibrary, onPlaceSelect]); return (React.createElement("div", { className: "place-autocomplete-card", style: { backgroundColor: '#fff', borderRadius: '5px', boxShadow: 'rgba(0, 0, 0, 0.35) 0px 5px 15px', margin: '10px', padding: '5px', fontFamily: 'Roboto, sans-serif', fontSize: 'small', width: '300px', } }, React.createElement("div", { ref: containerRef, style: { width: '100%' } }))); }; export default function App() { const [selectedPlace, setSelectedPlace] = useState(null); const [markerRef, marker] = useAdvancedMarkerRef(); return (React.createElement(APIProvider, { apiKey: API_KEY }, React.createElement(Map, { defaultCenter: { lat: 40.749933, lng: -73.98633 }, defaultZoom: 13, gestureHandling: 'greedy', mapId: "DEMO_MAP_ID", disableDefaultUI: true }, React.createElement(MapControl, { position: ControlPosition.BLOCK_START_INLINE_START }, React.createElement(PlaceAutocomplete, { onPlaceSelect: setSelectedPlace })), selectedPlace?.location && (React.createElement(AdvancedMarker, { ref: markerRef, position: selectedPlace.location })), selectedPlace?.location && marker && (React.createElement(InfoWindow, { anchor: marker }, React.createElement("div", null, React.createElement("span", { style: { fontWeight: 'bold' } }, selectedPlace.displayName ?? 'No name'), React.createElement("br", null), React.createElement("span", null, selectedPlace.formattedAddress ?? 'No address'))))))); } export function renderToDom(container) { const root = createRoot(container); root.render(React.createElement(React.StrictMode, null, React.createElement(App, null))); }
CSS
body { margin: 0; font-family: sans-serif; } #app { width: 100vw; height: 100vh; } .autocomplete-container input, .autocomplete-control { box-sizing: border-box; } .autocomplete-control { margin: 24px; background: #fff; } .autocomplete-container { width: 300px; } .autocomplete-container input { width: 100%; height: 40px; padding: 0 12px; font-size: 18px; } .autocomplete-container .custom-list { width: 100%; list-style: none; padding: 0; margin: 0; } .autocomplete-container .custom-list-item { padding: 8px; } .autocomplete-container .custom-list-item:hover { background: lightgrey; cursor: pointer; }
HTML
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no" />
<title>React - react place autocomplete map</title>
<style>
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
</style>
<script type="module">
import { renderToDom } from './src/app';
renderToDom(document.querySelector('#app'));
</script>
</head>
<body>
<div id="app"></div>
</body>
</html>Clone Sample
Git and Node.js are required to run this sample locally. Follow these instructions to install Node.js and NPM. The following commands clone, install dependencies and start the sample application.
git clone https://github.com/googlemaps-samples/js-api-samples.gitcd samples/rgm-autocompletenpm inpm start
Integration notes
When integrating Google Maps Place Autocomplete within a React application, this sample implements several key best practices and addresses common issues developers encounter with the new Places API.
1. Programmatic Instantiation
Instead of rendering the <gmp-place-autocomplete> Web Component directly in
JSX, this sample programmatically instantiates it using
new placesLibrary.PlaceAutocompleteElement() and appends it to a React ref.
- Issue: React's synthetic event system doesn't always seamlessly handle
custom Web Component events (like
gmp-select). Instantiating the element programmatically and attaching standard DOM event listeners ensures events are captured reliably.
2. Location Restriction & Cross-Context Objects
When placing the autocomplete Web Component outside the main DOM tree of the
map, it may lose automatic context of the map's viewport. To ensure that
search predictions are strictly biased or restricted to the map's current
bounds, this sample manually syncs the map's bounds to the autocomplete's
locationRestriction property.
- Issue: Passing complex Google Maps objects (like
LatLngBounds) directly across the React boundary can sometimes fail due to cross-contextinstanceofchecks. Always use.toJSON()(e.g.,map.getBounds().toJSON()) when assigning bounds to bypass these issues. This ensures users see search predictions relevant to their map view.
3. Handling Selections: toPlace() and fetchFields()
When a user selects an item from the autocomplete drop-down, the component fires
a gmp-select event containing a placePrediction.
- Issue: The prediction is not a fully populated Place object. You must
convert it using
placePrediction.toPlace()and then explicitly request the data you need by callingplace.fetchFields({ fields: ['location','displayName', 'formattedAddress'] }). - If you attempt to access a property on the
Placeobject that hasn't been fetched, it will be undefined or throw an error. This is a core design principle of the new Places API to ensure you only request (and pay for) the data you use.
4. Event Cleanup
Always remove standard DOM event listeners (e.g.,
autocomplete.removeEventListener) and Maps event listeners
(google.maps.event.removeListener) in your useEffect cleanup function to
prevent memory leaks when the React component unmounts.