はじめに
この記事では、ホテル検索アプリを作りながら Google Maps Platform の構成と使い方を記載します。
開発環境
- OS:Windows 11
- エディタ:VSCode
- Node.js:24.21.0
- Vite:8.3.0
- React:19.2.8
- @vis.gl/react-google-maps:1.10.1
アプリ概要と利用する Google Maps Platform API
左に検索フォームと結果一覧、右に地図を置いた画面を作成します。「駅近」などのキーワードで検索すると、条件と一致するホテルにピンが立ちます。ピンか一覧をクリックすると、ホテル名と住所の吹き出しが開きます。
Google Maps Platform は、機能ごとに4つの製品群に分かれています。今回使うのは Maps と Places の2つです。
アプリの各機能と、呼び出す API の対応は次のとおりです。
| 機能 | 利用API | Maps JavaScript API のクラス |
|---|---|---|
| 地図の表示 | Maps JavaScript API | google.maps.Map |
| ピンの表示 | Maps JavaScript API | google.maps.marker.AdvancedMarkerElement |
| 吹き出しの表示 | Maps JavaScript API | google.maps.InfoWindow |
| キーワード検索 | Places API (New) | google.maps.places.Place |
React から Maps JavaScript API を使うために、@vis.gl/react-google-maps を使います。Google と vis.gl チームが共同で開発しているライブラリです。Maps JavaScript API のクラスを React コンポーネントとして書けます。
| @vis.gl/react-google-maps | Maps JavaScript API の対応要素 |
|---|---|
<APIProvider> |
スクリプトの読み込み(google.maps.importLibrary) |
<Map> |
google.maps.Map |
<AdvancedMarker> |
google.maps.marker.AdvancedMarkerElement |
<Pin> |
google.maps.marker.PinElement |
<InfoWindow> |
google.maps.InfoWindow |
useMap() |
生成済みの google.maps.Map インスタンスの取得 |
useMapsLibrary("places") |
google.maps.importLibrary("places") |
手順1 Google Cloud 設定
Google Cloud Console で次の設定をします。
| 作業 | 設定画面 | 設定内容 |
|---|---|---|
| プロジェクト作成 | プロジェクトの選択 | 新規プロジェクトを作成し、請求先アカウントを紐付ける |
| API の有効化 | API とサービス > ライブラリ | Maps JavaScript API と Places API (New) を有効にする |
| API キーの作成 | API とサービス > 認証情報 | API キーを作成する |
| アプリケーションの制限 | API キーの編集画面 | ウェブサイト(HTTP リファラー)に http://localhost:5173/* を登録する |
| API の制限 | API キーの編集画面 | Maps JavaScript API と Places API (New) だけを許可する |
| Map ID の作成 | Google Maps Platform > マップ管理 | JavaScript 用のベクター地図の Map ID を作成する |
Maps JavaScript API のキーは、ブラウザに配信する JavaScript に含まれます。開発者ツールを開けば誰でも読めます。そのため、キーを隠すのではなく、呼び出せる URL と API を制限して不正利用を防ぎます。
Map ID は、Cloud Console で管理する地図設定の ID です。AdvancedMarker を使うには Map ID が必須です。開発中は Map ID を作らずに、テスト用の DEMO_MAP_ID を指定しても動きます。
手順2 プロジェクト作成
Vite の React + TypeScript テンプレートでプロジェクトを作ります。
npm create vite@latest google-maps-sample -- --template react-ts
cd google-maps-sample
npm install
npm install "@vis.gl/react-google-maps"
npm install -D @types/google.maps
@types/google.maps は、google.maps 名前空間の型定義です。テンプレートの tsconfig.app.json は types で読み込む型定義を指定しているため、追加しただけでは読み込まれません。types に "google.maps" を追記します。
{
"compilerOptions": {
"types": ["vite/client", "google.maps"]
}
}
API キーと Map ID は環境変数に置きます。
VITE_GOOGLE_MAPS_API_KEY=取得したAPIキー
VITE_GOOGLE_MAPS_MAP_ID=DEMO_MAP_ID
環境変数の型を宣言します。
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_GOOGLE_MAPS_API_KEY: string;
readonly VITE_GOOGLE_MAPS_MAP_ID: string;
}
設定値を1つのファイルにまとめます。初期表示の中心は東京駅にしています。
export const GOOGLE_MAPS_API_KEY = import.meta.env.VITE_GOOGLE_MAPS_API_KEY;
export const GOOGLE_MAPS_MAP_ID = import.meta.env.VITE_GOOGLE_MAPS_MAP_ID;
export const TOKYO_STATION = { lat: 35.681236, lng: 139.767125 };
テンプレートの CSS は地図の表示と合わないので、中身を置き換えます。
body {
margin: 0;
font-family: sans-serif;
}
手順3 地図表示
APIProvider で Maps JavaScript API を読み込み、その中に Map を置きます。
import { APIProvider, Map } from "@vis.gl/react-google-maps";
import { GOOGLE_MAPS_API_KEY, GOOGLE_MAPS_MAP_ID, TOKYO_STATION } from "./config";
export default function App() {
return (
<APIProvider apiKey={GOOGLE_MAPS_API_KEY} language="ja" region="JP">
<Map
style={{ width: "100vw", height: "100vh" }}
defaultCenter={TOKYO_STATION}
defaultZoom={15}
mapId={GOOGLE_MAPS_MAP_ID}
gestureHandling="greedy"
/>
</APIProvider>
);
}
APIProvider は Maps JavaScript API のスクリプトを1回だけ読み込みます。language と region は地図のラベルの言語と、地名表記の地域を指定します。
Map の中心座標とズームには、2種類の指定方法があります。
| props | 動作 | 用途 |
|---|---|---|
defaultCenter / defaultZoom
|
初回表示の値だけを渡す。ドラッグやズームは地図が自分で管理する | 利用者が自由に地図を動かす画面 |
center / zoom
|
React の state で値を管理する。変更時は onCameraChanged で state を更新する |
表示位置をアプリ側で同期したい画面 |
今回は利用者が自由に動かす画面なので default 付きを使います。center だけを渡して state を更新しないと、地図をドラッグしても元の位置に戻ります。最初はこの挙動で少し戸惑いました。
gestureHandling="greedy" は、Ctrl キーを押さずにマウスホイールでズームする設定です。
npm run dev
http://localhost:5173 を開くと、東京駅を中心にした地図が表示されます。
手順4 マーカー・吹き出し表示
マーカーは AdvancedMarker を使います。google.maps.Marker は2024年2月21日に非推奨となり、AdvancedMarkerElement の使用が推奨されています。@vis.gl/react-google-maps にも古い Marker コンポーネントが残っていますが、新規開発では使いません。
マーカーと吹き出しを1つのコンポーネントにまとめます。
import {
AdvancedMarker,
InfoWindow,
Pin,
useAdvancedMarkerRef,
} from "@vis.gl/react-google-maps";
type Props = {
position: { lat: number; lng: number };
title: string;
description?: string;
selected: boolean;
onSelect: () => void;
onClose: () => void;
};
export function PlaceMarker({
position,
title,
description,
selected,
onSelect,
onClose,
}: Props) {
// 吹き出しの表示位置を決めるため、マーカーのインスタンスを取得する
const [markerRef, marker] = useAdvancedMarkerRef();
return (
<>
<AdvancedMarker ref={markerRef} position={position} title={title} onClick={onSelect}>
<Pin
background={selected ? "#d93025" : "#1a73e8"}
borderColor="#ffffff"
glyphColor="#ffffff"
/>
</AdvancedMarker>
{selected && (
<InfoWindow anchor={marker} onCloseClick={onClose}>
<strong>{title}</strong>
{description && <p>{description}</p>}
</InfoWindow>
)}
</>
);
}
InfoWindow の anchor にマーカーを渡すと、吹き出しがマーカーの真上に表示されます。Pin は標準のピン形状の色を変えるコンポーネントです。選択中のピンを赤、それ以外を青にしました。
動作確認のため、東京駅に1本だけマーカーを立てます。
import { useState } from "react";
import { APIProvider, Map } from "@vis.gl/react-google-maps";
import { PlaceMarker } from "./components/PlaceMarker";
import { GOOGLE_MAPS_API_KEY, GOOGLE_MAPS_MAP_ID, TOKYO_STATION } from "./config";
export default function App() {
const [selected, setSelected] = useState(false);
return (
<APIProvider apiKey={GOOGLE_MAPS_API_KEY} language="ja" region="JP">
<Map
style={{ width: "100vw", height: "100vh" }}
defaultCenter={TOKYO_STATION}
defaultZoom={15}
mapId={GOOGLE_MAPS_MAP_ID}
gestureHandling="greedy"
>
<PlaceMarker
position={TOKYO_STATION}
title="東京駅"
description="東京都千代田区丸の内1丁目"
selected={selected}
onSelect={() => setSelected(true)}
onClose={() => setSelected(false)}
/>
</Map>
</APIProvider>
);
}
ピンをクリックすると吹き出しが開き、×ボタンで閉じます。
手順5 ホテル検索(Places API (New))
場所の検索には google.maps.places.Place クラスを使います。古い PlacesService は、2025年3月1日以降、新規利用者には提供されていません。
| 用途 | 旧 API(新規利用不可) | 新 API |
|---|---|---|
| テキスト検索・周辺検索 | PlacesService |
Place.searchByText / Place.searchNearby
|
| 入力補完のデータ取得 | AutocompleteService |
AutocompleteSuggestion |
| 入力補完の UI | Autocomplete |
PlaceAutocompleteElement |
Place.searchByText では、取得するデータ項目を fields で必ず指定します。リクエストは、指定した項目の中で最も高い SKU の料金で課金されます。今回はホテル名・座標・住所の3項目だけを取得します。
検索処理をカスタムフックにまとめます。
import { useState } from "react";
import { useMap, useMapsLibrary } from "@vis.gl/react-google-maps";
export type SearchResult = {
id: string;
name: string;
address: string;
position: { lat: number; lng: number };
};
export function useTextSearch() {
const map = useMap();
// places ライブラリの読み込みが終わるまでは null が返る
const placesLib = useMapsLibrary("places");
const [results, setResults] = useState<SearchResult[]>([]);
const [error, setError] = useState<string | null>(null);
const search = async (query: string) => {
if (!map || !placesLib || query.trim() === "") return;
setError(null);
try {
const { places } = await placesLib.Place.searchByText({
textQuery: query,
// 検索対象をホテルに限定する
includedType: "hotel",
useStrictTypeFiltering: true,
// 課金額はここで指定した項目で決まる
fields: ["displayName", "location", "formattedAddress"],
locationBias: map.getCenter(),
language: "ja",
region: "jp",
maxResultCount: 10,
});
const next: SearchResult[] = places.flatMap((place) =>
place.location
? [
{
id: place.id,
name: place.displayName ?? "",
address: place.formattedAddress ?? "",
position: place.location.toJSON(),
},
]
: [],
);
setResults(next);
// すべての検索結果が画面に入るように表示位置を調整する
if (next.length > 0) {
const bounds = new google.maps.LatLngBounds();
next.forEach((result) => bounds.extend(result.position));
map.fitBounds(bounds);
}
} catch (e) {
setError(e instanceof Error ? e.message : "検索に失敗しました");
}
};
return { results, error, search };
}
includedType に場所の種類を指定すると、その種類の場所を優先して返します。useStrictTypeFiltering を true にすると、指定した種類以外は結果から除外されます。今回は "hotel" を指定して、ホテルだけを検索対象にしました。
宿泊施設の種類は、Places API では次のように分かれています。
| 指定値 | 対象 |
|---|---|
hotel |
ホテル |
japanese_inn |
旅館 |
hostel |
ホステル |
lodging |
宿泊施設全般 |
旅館やホステルも検索対象に含める場合は、"lodging" を指定します。
locationBias に地図の中心座標を渡すと、中心に近い場所が優先して返ります。地図を別の場所に動かしてから検索すると、その周辺のホテルが返ります。範囲外の場所も結果に含まれます。範囲内に絞る場合は locationRestriction を使います。
useMap() は、Map コンポーネントの外で呼んでも地図インスタンスを取得できます。id を指定しない Map は default という ID で登録されるためです。この仕組みにより、検索フォームと地図を別の場所に置けます。
検索フォーム・結果一覧・地図をまとめたコンポーネントを作ります。
import { useState, type FormEvent } from "react";
import { Map } from "@vis.gl/react-google-maps";
import { PlaceMarker } from "./PlaceMarker";
import { useTextSearch } from "../hooks/useTextSearch";
import { GOOGLE_MAPS_MAP_ID, TOKYO_STATION } from "../config";
export function PlaceFinder() {
const { results, error, search } = useTextSearch();
const [query, setQuery] = useState("ホテル");
const [selectedId, setSelectedId] = useState<string | null>(null);
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setSelectedId(null);
void search(query);
};
return (
<div style={{ display: "grid", gridTemplateColumns: "320px 1fr", height: "100vh" }}>
<aside style={{ padding: 16, overflowY: "auto" }}>
<form onSubmit={handleSubmit}>
<input
value={query}
placeholder="例:駅近"
onChange={(e) => setQuery(e.target.value)}
/>
<button type="submit">検索</button>
</form>
{error && <p role="alert">{error}</p>}
<ul>
{results.map((result) => (
<li key={result.id}>
<button type="button" onClick={() => setSelectedId(result.id)}>
{result.name}
</button>
</li>
))}
</ul>
</aside>
<Map
defaultCenter={TOKYO_STATION}
defaultZoom={15}
mapId={GOOGLE_MAPS_MAP_ID}
gestureHandling="greedy"
>
{results.map((result) => (
<PlaceMarker
key={result.id}
position={result.position}
title={result.name}
description={result.address}
selected={result.id === selectedId}
onSelect={() => setSelectedId(result.id)}
onClose={() => setSelectedId(null)}
/>
))}
</Map>
</div>
);
}
選択中のホテルの ID を PlaceFinder の state で持ちます。一覧のボタンとマーカーのどちらをクリックしても、同じ state が更新されます。
App.tsx は APIProvider の中に PlaceFinder を置くだけになります。
import { APIProvider } from "@vis.gl/react-google-maps";
import { PlaceFinder } from "./components/PlaceFinder";
import { GOOGLE_MAPS_API_KEY } from "./config";
export default function App() {
return (
<APIProvider apiKey={GOOGLE_MAPS_API_KEY} language="ja" region="JP">
<PlaceFinder />
</APIProvider>
);
}
npm run dev
初期表示のまま「駅近」で検索すると、東京駅周辺で条件と合致するホテルにピンが立ちます。一覧のホテル名をクリックすると、該当するピンが赤くなり、吹き出しが開きます。
まとめ
この記事では、ホテル検索アプリの作成を通して Google Maps Platform の構成と使い方について記載しました。地図とマーカーは Maps JavaScript API、場所の検索は Places API (New) が担当し、React からは @vis.gl/react-google-maps のコンポーネントとフックで呼び出せます。
参考
- Pricing categories | Google Maps Platform
- Place Data Fields (New) | Places API
- Place Types | Maps JavaScript API
- Text Search (New) | Maps JavaScript API
- Migrate to the new Place Search | Maps JavaScript API
- Deprecations | Google Maps Platform
- @vis.gl/react-google-maps | npm
- visgl/react-google-maps | GitHub
- Vite 8.0 is out!


