React Google Maps Library - Place Autocomplete

  • 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-maps library, 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-maps library 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.git
  cd samples/rgm-autocomplete
  npm i
  npm 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-context instanceof checks. 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 calling place.fetchFields({ fields: ['location', 'displayName', 'formattedAddress'] }).
  • If you attempt to access a property on the Place object 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.