This example shows how to build a basic locations services web app using the
[Google Maps Platform's Extended Component Library](https://github.com/googlemaps/extended-component-library/tree/main)
with the [vis.gl/react-google-maps](https://visgl.github.io/react-google-maps/)
open source library.

[Google Maps Platform's Extended Component
Library](https://github.com/googlemaps/extended-component-library/tree/main) is
a set of Web Components that helps developers build better maps faster, and with
less effort. It encapsulates boilerplate code, best practices, and responsive
design, reducing complex map UIs into what is effectively a single HTML element.
These components make it easier to read, learn, customize, and maintain
maps-related code.

The [vis.gl/react-google-maps](https://visgl.github.io/react-google-maps/)
library is a collection of React components and hooks for the Google Maps
JavaScript API.

> [!IMPORTANT]
> **Important:** The [vis.gl/react-google-maps](https://visgl.github.io/react-google-maps/) library and the [Extended Component
> Library](https://github.com/googlemaps/extended-component-library/tree/main) are offered using an open source license. It is not governed by the Google Maps Platform Support Technical Support Services Guidelines, the SLA, or the Deprecation Policy (however, any Google Maps Platform services used by the library remain subject to the Google Maps Platform Terms of Service). If you find a bug, or have a feature request, file an issue on the respective library site.

<iframe src="https://maps-docs-team.web.app/samples/rgm-college-picker/dist/" allow="fullscreen; "></iframe>

### TypeScript

```jsx
import React, { useState, useRef, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import {
    AdvancedMarker,
    Map,
    Pin,
    APIProvider,
} from '@vis.gl/react-google-maps';
import {
    PlaceReviews,
    PlaceDataProvider,
    PlaceDirectionsButton,
    IconButton,
    PlaceOverview,
    SplitLayout,
    OverlayLayout,
    PlacePicker,
} from '@googlemaps/extended-component-library/react';

/**
 * The below imports are necessary because we are creating refs of
 * the OverlayLayout and PlacePicker components. You need to pass
 * the ref property a web component type object. Imports from  
 * @googlemaps/extended-component-library/react are wrappers around
 * the web components, not the components themselves. For the ref
 * property we import the actual components and alias them for clarity.
 */
import { OverlayLayout as TOverlayLayout } from '@googlemaps/extended-component-library/overlay_layout.js';
import { PlacePicker as TPlacePicker } from '@googlemaps/extended-component-library/place_picker.js';

const COUNTRIES = ['us', 'ca'];
const API_KEY = 'GOOGLE_MAPS_API_KEY';
const DEFAULT_CENTER = { lat: 38, lng: -98 };
const DEFAULT_ZOOM = 4;
const DEFAULT_ZOOM_WITH_LOCATION = 16;

/**
 * Sample app that helps users locate a college on the map, with place info such
 * as ratings, photos, and reviews displayed on the side.
 */
export default function App() {
    const overlayLayoutRef = useRef<TOverlayLayout>(null);
    const pickerRef = useRef<TPlacePicker>(null);
    const [college, setCollege] = useState<
        google.maps.places.Place | undefined
    >(undefined);

    /**
     * We track the map's camera state separately and use an onCameraChanged listener.
     * This prevents the map from becoming strictly "controlled" by college.location,
     * which would otherwise lock the camera and prevent the user from panning or zooming.
     */
    const [cameraProps, setCameraProps] = useState({
        center: DEFAULT_CENTER,
        zoom: DEFAULT_ZOOM,
    });

    useEffect(() => {
        if (college?.location) {
            setCameraProps({
                center: {
                    lat: college.location.lat(),
                    lng: college.location.lng(),
                },
                zoom: DEFAULT_ZOOM_WITH_LOCATION,
            });
        }
    }, [college]);

    /**
     * See https://lit.dev/docs/frameworks/react/#using-slots for why
     * we need to wrap our custom elements in a div with a slot attribute.
     */
    return (
        <div className="App">
            <APIProvider
                solutionChannel="GMP_devsite_samples_v3_rgmcollegepicker"
                apiKey={API_KEY}
                version="beta">
                <SplitLayout rowReverse rowLayoutMinWidth={700}>
                    <div className="SlotDiv" slot="fixed">
                        <OverlayLayout ref={overlayLayoutRef}>
                            <div className="SlotDiv" slot="main">
                                <PlacePicker
                                    className="CollegePicker"
                                    ref={pickerRef}
                                    forMap="gmap"
                                    country={COUNTRIES}
                                    type="university"
                                    placeholder="Enter a college in the US or Canada"
                                    onPlaceChange={() => {
                                        if (!pickerRef.current?.value) {
                                            setCollege(undefined);
                                        } else {
                                            setCollege(pickerRef.current.value);
                                        }
                                    }}
                                />
                                <PlaceOverview
                                    size="large"
                                    place={college}
                                    googleLogoAlreadyDisplayed>
                                    <div slot="action" className="SlotDiv">
                                        <IconButton
                                            slot="action"
                                            variant="filled"
                                            onClick={() => {
                                                if (overlayLayoutRef.current)
                                                    void overlayLayoutRef.current.showOverlay();
                                            }}>
                                            See Reviews
                                        </IconButton>
                                    </div>
                                    <div slot="action" className="SlotDiv">
                                        <PlaceDirectionsButton
                                            slot="action"
                                            variant="filled">
                                            Directions
                                        </PlaceDirectionsButton>
                                    </div>
                                </PlaceOverview>
                            </div>
                            <div slot="overlay" className="SlotDiv">
                                <IconButton
                                    className="CloseButton"
                                    onClick={() => {
                                        if (overlayLayoutRef.current)
                                            void overlayLayoutRef.current.hideOverlay();
                                    }}>
                                    Close
                                </IconButton>
                                <PlaceDataProvider place={college}>
                                    <PlaceReviews />
                                </PlaceDataProvider>
                            </div>
                        </OverlayLayout>
                    </div>
                    <div className="SplitLayoutContainer" slot="main">
                        <Map
                            id="gmap"
                            mapId="8c732c82e4ec29d9"
                            {...cameraProps}
                            onCameraChanged={(ev) => {
                                setCameraProps(ev.detail);
                            }}>
                            {college?.location && (
                                <AdvancedMarker position={college.location}>
                                    <Pin
                                        background={'#FBBC04'}
                                        glyphColor={'#000'}
                                        borderColor={'#000'}
                                    />
                                </AdvancedMarker>
                            )}
                        </Map>
                    </div>
                </SplitLayout>
            </APIProvider>
        </div>
    );
}

export function renderToDom(container: HTMLElement) {
    const root = createRoot(container);
    root.render(
        <React.StrictMode>
            <App />
        </React.StrictMode>
    );
}
```

### JavaScript

```javascript
import React, { useState, useRef, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import { AdvancedMarker, Map, Pin, APIProvider, } from '@vis.gl/react-google-maps';
import { PlaceReviews, PlaceDataProvider, PlaceDirectionsButton, IconButton, PlaceOverview, SplitLayout, OverlayLayout, PlacePicker, } from '@googlemaps/extended-component-library/react';
const COUNTRIES = ['us', 'ca'];
const API_KEY = 'GOOGLE_MAPS_API_KEY';
const DEFAULT_CENTER = { lat: 38, lng: -98 };
const DEFAULT_ZOOM = 4;
const DEFAULT_ZOOM_WITH_LOCATION = 16;
/**
 * Sample app that helps users locate a college on the map, with place info such
 * as ratings, photos, and reviews displayed on the side.
 */
export default function App() {
    const overlayLayoutRef = useRef(null);
    const pickerRef = useRef(null);
    const [college, setCollege] = useState(undefined);
    /**
     * We track the map's camera state separately and use an onCameraChanged listener.
     * This prevents the map from becoming strictly "controlled" by college.location,
     * which would otherwise lock the camera and prevent the user from panning or zooming.
     */
    const [cameraProps, setCameraProps] = useState({
        center: DEFAULT_CENTER,
        zoom: DEFAULT_ZOOM,
    });
    useEffect(() => {
        if (college?.location) {
            setCameraProps({
                center: {
                    lat: college.location.lat(),
                    lng: college.location.lng(),
                },
                zoom: DEFAULT_ZOOM_WITH_LOCATION,
            });
        }
    }, [college]);
    /**
     * See https://lit.dev/docs/frameworks/react/#using-slots for why
     * we need to wrap our custom elements in a div with a slot attribute.
     */
    return (React.createElement("div", { className: "App" },
        React.createElement(APIProvider, { solutionChannel: "GMP_devsite_samples_v3_rgmcollegepicker", apiKey: API_KEY, version: "beta" },
            React.createElement(SplitLayout, { rowReverse: true, rowLayoutMinWidth: 700 },
                React.createElement("div", { className: "SlotDiv", slot: "fixed" },
                    React.createElement(OverlayLayout, { ref: overlayLayoutRef },
                        React.createElement("div", { className: "SlotDiv", slot: "main" },
                            React.createElement(PlacePicker, { className: "CollegePicker", ref: pickerRef, forMap: "gmap", country: COUNTRIES, type: "university", placeholder: "Enter a college in the US or Canada", onPlaceChange: () => {
                                    if (!pickerRef.current?.value) {
                                        setCollege(undefined);
                                    }
                                    else {
                                        setCollege(pickerRef.current.value);
                                    }
                                } }),
                            React.createElement(PlaceOverview, { size: "large", place: college, googleLogoAlreadyDisplayed: true },
                                React.createElement("div", { slot: "action", className: "SlotDiv" },
                                    React.createElement(IconButton, { slot: "action", variant: "filled", onClick: () => {
                                            if (overlayLayoutRef.current)
                                                void overlayLayoutRef.current.showOverlay();
                                        } }, "See Reviews")),
                                React.createElement("div", { slot: "action", className: "SlotDiv" },
                                    React.createElement(PlaceDirectionsButton, { slot: "action", variant: "filled" }, "Directions")))),
                        React.createElement("div", { slot: "overlay", className: "SlotDiv" },
                            React.createElement(IconButton, { className: "CloseButton", onClick: () => {
                                    if (overlayLayoutRef.current)
                                        void overlayLayoutRef.current.hideOverlay();
                                } }, "Close"),
                            React.createElement(PlaceDataProvider, { place: college },
                                React.createElement(PlaceReviews, null))))),
                React.createElement("div", { className: "SplitLayoutContainer", slot: "main" },
                    React.createElement(Map, { id: "gmap", mapId: "8c732c82e4ec29d9", ...cameraProps, onCameraChanged: (ev) => {
                            setCameraProps(ev.detail);
                        } }, college?.location && (React.createElement(AdvancedMarker, { position: college.location },
                        React.createElement(Pin, { background: '#FBBC04', glyphColor: '#000', borderColor: '#000' })))))))));
}
export function renderToDom(container) {
    const root = createRoot(container);
    root.render(React.createElement(React.StrictMode, null,
        React.createElement(App, null)));
}
```

### CSS

```css
body {
    margin: 0;
    font-family: sans-serif;
}
#app {
    width: 100vw;
    height: 100vh;
}

:root {
    --gmpx-fixed-panel-width-row-layout: 450px;
}

.App {
    --gmpx-color-surface: #f6f5ff;
    --gmpx-color-on-primary: #f8e8ff;
    --gmpx-color-on-surface: #000;
    --gmpx-color-on-surface-variant: #636268;
    --gmpx-color-primary: #8a5cf4;
    --gmpx-fixed-panel-height-column-layout: 420px;
    background: var(--gmpx-color-surface);
    inset: 0;
    position: fixed;
}

.MainContainer {
    display: flex;
    flex-direction: column;
}

.SplitLayoutContainer {
    height: 100%;
}

/* 
 * Because the parent uses display: contents, flex-grow won't work.
 * We explicitly set the width here to stretch the search box, which 
 * also ensures the Maps Autocomplete dropdown matches this width.
 */
.CollegePicker {
    --gmpx-color-surface: #fff;
    margin: 1rem;
    width: calc(100% - 2rem);
    box-sizing: border-box;
}

.CloseButton {
    display: block;
    margin: 1rem;
}

.SlotDiv {
    display: contents;
}
```

### HTML

```html
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <!-- Prevents reviewer profile photos from breaking due to cross-origin Referer restrictions -->
        <meta name="referrer" content="no-referrer" />
        <meta
            name="viewport"
            content="width=device-width, initial-scale=1.0, user-scalable=no" />
        <title>React - College Picker</title>
        <meta name="description" content="React College Picker Example" />
        <link rel="stylesheet" href="./style.css" />
        <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](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
to install Node.js and NPM. You will also need a [Google Maps API key](https://developers.google.com/maps/documentation/javascript/get-api-key.md.txt).


The following commands clone the repository, install dependencies, and copy the environment file.

```javascript
  git clone https://github.com/googlemaps-samples/js-api-samples.git
  cd js-api-samples
  npm i
  cp .env.template .env
```


Once you copy the environment file, make sure to open the `.env` file and replace `<Insert your own development API key here>` with your actual API key before starting the sample.

```javascript
  cd samples/rgm-college-picker
  npm start
```

## Integration notes

When integrating Google Maps Web Components (Extended Component Library) and
`vis.gl/react-google-maps` within a React application, you may encounter
friction between React's declarative state model and built-in browser APIs. This
section describes the specific workarounds implemented in this sample to ensure
expected behavior.

### Use slots for web component composition

React (prior to v19) struggles to pass the standard HTML `slot`
attribute directly to nested Web Components.

To work around this, wrap the slotted elements inside a standard HTML `<div>`
(for example, `<div className="SlotDiv" slot="main">`).

If the parent component uses Flexbox, adding an intermediate `<div>` can
disrupt the layout hierarchy. To avoid this, apply `display: contents;` to the
wrapper `<div>`. This removes the wrapper's box from the layout tree, allowing
child components to participate directly in the parent's Flexbox layout.

**Caveat with Web Component sizing:**
Because `display: contents` causes the wrapper `<div>` to generate no box, any
styling (such as `width`, `margin`, or `flex-grow`) applied directly to the
wrapper `<div>` is ignored. Furthermore, custom elements default to
`display: inline-block` and won't automatically stretch to fill available
horizontal space.

To ensure proper layout, apply styles directly to the child component host:

```css
.CollegePicker {
    width: calc(100% - 2rem);
    box-sizing: border-box;
}
```

Because the `<PlacePicker>` autocomplete drop-down matches the physical width
of its host input, properly sizing the host component resolves drop-down
alignment without requiring global CSS overrides on `.pac-container`.

### Retain Map Pan \& Zoom (Controlled Camera State)

Passing the location directly to the map (for example,
`<Map center={college.location} />`) creates a strictly "Controlled Component"
in `@vis.gl/react-google-maps`. Anytime the user tries to pan the map, React
immediately snaps the camera back to the locked `college.location` coordinate,
effectively breaking panning and zooming.

To solve this, decouple the map's camera from the selected place. In this
example, we did the following:

1. Created a dedicated `cameraProps` React state to track the map's current center and zoom.
2. Added an `onCameraChanged` event listener to the map so it could smoothly update its own state while the user dragged it.
3. Created a `useEffect` hook that explicitly intercepts a new college selection and programmatically updates the `cameraProps`. This allows the map to fly to new searches while remaining fully interactive.

### Use stable references to prevent render loops

Passing inline array or object literals directly to component props (for
example, `<PlacePicker country={['us', 'ca']} />`) creates a new object
reference in memory on every render. Because React wrappers for Web Components
and hooks often synchronize properties or trigger effects based on reference
equality (`===`), an unstable reference makes the component treat the property
as constantly changing. This can trigger continuous updates, redundant network
requests, or infinite re-render loops.

To avoid this, ensure that non-primitive props maintain stable references across
renders:

- Define static arrays or configuration objects outside the component scope (for example, `const COUNTRIES = ['us', 'ca'];`).
- For dynamic objects or arrays that depend on props or state, memoize them using the `useMemo` hook.

In this example, we extracted the allowed countries into a top-level `COUNTRIES`
constant and passed `country={COUNTRIES}`, providing a stable reference that
prevents repeated updates.

### Ensure that images load in the Shadow DOM

Google's image servers (`lh3.googleusercontent.com`) block requests that
include third-party `Referer` headers to prevent unauthorized
hotlinking. Because browsers send the origin in the `Referer` header by
default, loading reviewer avatars from any third-party domain (both in
production and on `localhost`) returns an HTTP 403 Forbidden error, which
causes images to fail to load.

Because the underlying `<img>` tags reside inside the Web Component's Shadow
DOM, you cannot directly add `referrerpolicy="no-referrer"` to the individual
image elements.

To fix this, apply a document-level referrer policy in the `<head>` of your
HTML page:

```html
<meta name="referrer" content="no-referrer" />
```

This instructs the browser to omit the `Referer` header for outgoing requests,
allowing images to load correctly.

### Fix Implicit Void Returns in React Handlers

Using arrow function shorthands for event handlers that return Promises or
`void` (e.g., `onClick={() => overlayLayoutRef.current.showOverlay()}`)
causes type errors in TypeScript.

To fix this, wrap event handlers in curly braces `{ ... }` and use the `void`
operator for fire-and-forget Promises. This prevents accidental return values
from being passed to React's event system.