This skill should be used when the user asks to "add Google Maps", "integrate maps", "show location on map", "implement place search", "add route calculation", "geocode address", "display...
Next.js App Router で @react-google-maps/api を使用した Google Maps 統合ガイド。
@react-google-maps/api v2.xnpm install @react-google-maps/api でライブラリをインストール.env.local に NEXT_PUBLIC_GOOGLE_MAPS_API_KEY を設定npm install @react-google-maps/api
# または
yarn add @react-google-maps/api
# または
pnpm add @react-google-maps/api
.env.local ファイルを作成:
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=your_api_key_here
注意:
NEXT_PUBLIC_プレフィックスが必要です。これによりクライアントサイドで使用可能になります。
必要に応じて以下の API を有効化:
| API | 用途 |
|---|---|
| Maps JavaScript API | 地図の表示(必須) |
| Places API | 場所検索・オートコンプリート |
| Directions API | ルート計算 |
| Geocoding API | 住所⇔座標変換 |
localhost:*, your-domain.com/*// components/google-maps-provider.tsx
'use client';
import { Libraries, useLoadScript } from '@react-google-maps/api';
import { createContext, useContext, ReactNode } from 'react';
const libraries: Libraries = ['places', 'drawing', 'visualization'];
interface GoogleMapsContextType {
isLoaded: boolean;
}
const GoogleMapsContext = createContext<GoogleMapsContextType>({ isLoaded: false });
export function GoogleMapsProvider({ children }: { children: ReactNode }) {
const { isLoaded, loadError } = useLoadScript({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!,
libraries,
});
if (loadError) {
return <div className="p-4 text-red-500">Google Maps の読み込みに失敗しました</div>;
}
if (!isLoaded) {
return <div className="p-4">地図を読み込み中...</div>;
}
return (
<GoogleMapsContext.Provider value={{ isLoaded }}>
{children}
</GoogleMapsContext.Provider>
);
}
export function useGoogleMapsContext() {
return useContext(GoogleMapsContext);
}
詳細: examples/google-maps-provider.tsx
// components/map.tsx
'use client';
import { GoogleMap } from '@react-google-maps/api';
import { useCallback, useState } from 'react';
const containerStyle = {
width: '100%',
height: '400px',
};
const defaultCenter = {
lat: 35.6812,
lng: 139.7671,
};
export function Map() {
const [map, setMap] = useState<google.maps.Map | null>(null);
const onLoad = useCallback((map: google.maps.Map) => {
setMap(map);
}, []);
const onUnmount = useCallback(() => {
setMap(null);
}, []);
return (
<GoogleMap
mapContainerStyle={containerStyle}
center={defaultCenter}
zoom={15}
onLoad={onLoad}
onUnmount={onUnmount}
/>
);
}
// app/map/layout.tsx
import { GoogleMapsProvider } from '@/components/google-maps-provider';
export default function MapLayout({ children }: { children: React.ReactNode }) {
return (
<GoogleMapsProvider>
{children}
</GoogleMapsProvider>
);
}
import { GoogleMap, Marker, InfoWindow } from '@react-google-maps/api';
import { useState } from 'react';
interface Location {
id: string;
position: google.maps.LatLngLiteral;
title: string;
}
const locations: Location[] = [
{ id: '1', position: { lat: 35.6812, lng: 139.7671 }, title: '東京駅' },
{ id: '2', position: { lat: 35.6586, lng: 139.7454 }, title: '東京タワー' },
];
export function MapWithMarkers() {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<GoogleMap mapContainerStyle={containerStyle} center={locations[0].position} zoom={13}>
{locations.map((location) => (
<Marker
key={location.id}
position={location.position}
onClick={() => setSelectedId(location.id)}
>
{selectedId === location.id && (
<InfoWindow onCloseClick={() => setSelectedId(null)}>
<div className="p-2">{location.title}</div>
</InfoWindow>
)}
</Marker>
))}
</GoogleMap>
);
}
詳細: examples/map-with-markers.tsx
import { Autocomplete } from '@react-google-maps/api';
import { useCallback, useState } from 'react';
export function PlaceAutocomplete({
onPlaceSelect
}: {
onPlaceSelect: (place: google.maps.places.PlaceResult) => void
}) {
const [autocomplete, setAutocomplete] = useState<google.maps.places.Autocomplete | null>(null);
const onLoad = useCallback((ac: google.maps.places.Autocomplete) => {
setAutocomplete(ac);
}, []);
const onPlaceChanged = useCallback(() => {
if (autocomplete) {
const place = autocomplete.getPlace();
if (place.geometry?.location) {
onPlaceSelect(place);
}
}
}, [autocomplete, onPlaceSelect]);
return (
<Autocomplete
onLoad={onLoad}
onPlaceChanged={onPlaceChanged}
options={{
componentRestrictions: { country: 'jp' },
types: ['establishment'],
}}
>
<input
type="text"
placeholder="場所を検索..."
className="w-full px-4 py-2 border rounded-lg"
/>
</Autocomplete>
);
}
詳細: examples/places-autocomplete.tsx, references/places-api.md
import { DirectionsRenderer, DirectionsService } from '@react-google-maps/api';
import { useCallback, useState } from 'react';
export function DirectionsMap({
origin,
destination,
}: {
origin: google.maps.LatLngLiteral;
destination: google.maps.LatLngLiteral;
}) {
const [directions, setDirections] = useState<google.maps.DirectionsResult | null>(null);
const directionsCallback = useCallback(
(result: google.maps.DirectionsResult | null, status: google.maps.DirectionsStatus) => {
if (status === 'OK' && result) {
setDirections(result);
}
},
[]
);
return (
<GoogleMap mapContainerStyle={containerStyle} center={origin} zoom={12}>
{!directions && (
<DirectionsService
options={{
origin,
destination,
travelMode: google.maps.TravelMode.DRIVING,
}}
callback={directionsCallback}
/>
)}
{directions && <DirectionsRenderer directions={directions} />}
</GoogleMap>
);
}
詳細: examples/directions-route.tsx, references/directions-api.md
// utils/geocoding.ts
export async function geocodeAddress(address: string): Promise<google.maps.LatLngLiteral | null> {
const geocoder = new google.maps.Geocoder();
return new Promise((resolve) => {
geocoder.geocode({ address }, (results, status) => {
if (status === 'OK' && results?.[0]) {
const location = results[0].geometry.location;
resolve({ lat: location.lat(), lng: location.lng() });
} else {
resolve(null);
}
});
});
}
export async function reverseGeocode(
location: google.maps.LatLngLiteral
): Promise<string | null> {
const geocoder = new google.maps.Geocoder();
return new Promise((resolve) => {
geocoder.geocode({ location }, (results, status) => {
if (status === 'OK' && results?.[0]) {
resolve(results[0].formatted_address);
} else {
resolve(null);
}
});
});
}
詳細: examples/geocoding-service.ts, references/geocoding-api.md
import { HeatmapLayer } from '@react-google-maps/api';
const heatmapData = [
new google.maps.LatLng(35.6812, 139.7671),
new google.maps.LatLng(35.6586, 139.7454),
// ... more points
];
export function HeatmapMap() {
return (
<GoogleMap mapContainerStyle={containerStyle} center={defaultCenter} zoom={12}>
<HeatmapLayer
data={heatmapData}
options={{
radius: 20,
opacity: 0.7,
}}
/>
</GoogleMap>
);
}
詳細: examples/heatmap-layer.tsx, references/visualization.md
import { MarkerClusterer } from '@react-google-maps/api';
export function ClusteredMap({ locations }: { locations: Location[] }) {
return (
<GoogleMap mapContainerStyle={containerStyle} center={defaultCenter} zoom={10}>
<MarkerClusterer>
{(clusterer) =>
locations.map((location) => (
<Marker
key={location.id}
position={location.position}
clusterer={clusterer}
/>
))
}
</MarkerClusterer>
</GoogleMap>
);
}
詳細: examples/marker-clusterer.tsx
import { StreetViewPanorama } from '@react-google-maps/api';
export function StreetViewMap({ position }: { position: google.maps.LatLngLiteral }) {
return (
<GoogleMap mapContainerStyle={containerStyle} center={position} zoom={15}>
<StreetViewPanorama
position={position}
visible={true}
options={{
addressControl: true,
fullscreenControl: true,
}}
/>
</GoogleMap>
);
}
詳細: examples/street-view.tsx, references/streetview.md
import { DrawingManager } from '@react-google-maps/api';
export function DrawingMap() {
const onPolygonComplete = (polygon: google.maps.Polygon) => {
const path = polygon.getPath();
const coordinates = path.getArray().map((latLng) => ({
lat: latLng.lat(),
lng: latLng.lng(),
}));
console.log('Polygon coordinates:', coordinates);
};
return (
<GoogleMap mapContainerStyle={containerStyle} center={defaultCenter} zoom={15}>
<DrawingManager
drawingMode={google.maps.drawing.OverlayType.POLYGON}
onPolygonComplete={onPolygonComplete}
options={{
drawingControl: true,
drawingControlOptions: {
position: google.maps.ControlPosition.TOP_CENTER,
drawingModes: [
google.maps.drawing.OverlayType.POLYGON,
google.maps.drawing.OverlayType.POLYLINE,
google.maps.drawing.OverlayType.CIRCLE,
],
},
}}
/>
</GoogleMap>
);
}
詳細: examples/drawing-manager.tsx, references/drawing-tools.md
import dynamic from 'next/dynamic';
const MapComponent = dynamic(
() => import('@/components/map').then((mod) => mod.Map),
{
ssr: false,
loading: () => <div className="h-[400px] bg-gray-100 animate-pulse" />
}
);
// Map インスタンスを ref で保持し、再レンダリング時の再生成を防ぐ
const mapRef = useRef<google.maps.Map | null>(null);
const onLoad = useCallback((map: google.maps.Map) => {
mapRef.current = map;
}, []);
export function handleGoogleMapsError(error: unknown): string {
if (error instanceof Error) {
const message = error.message;
if (message.includes('RefererNotAllowedMapError')) {
return 'API キーのリファラー制限を確認してください';
}
if (message.includes('InvalidKeyMapError')) {
return 'API キーが無効です';
}
if (message.includes('ApiNotActivatedMapError')) {
return '必要な API が有効化されていません';
}
if (message.includes('OverQueryLimitMapError')) {
return 'API 呼び出し制限を超えました';
}
}
return '不明なエラーが発生しました';
}
詳細: references/troubleshooting.md
// types/google-maps.ts
export interface MapLocation {
id: string;
position: google.maps.LatLngLiteral;
title: string;
description?: string;
icon?: string;
}
export interface DirectionsRequest {
origin: google.maps.LatLngLiteral | string;
destination: google.maps.LatLngLiteral | string;
travelMode: google.maps.TravelMode;
waypoints?: google.maps.DirectionsWaypoint[];
}
export interface GeocodingResult {
address: string;
location: google.maps.LatLngLiteral;
placeId: string;
formattedAddress: string;
}
| コンポーネント | 用途 |
|---|---|
GoogleMap |
地図の表示 |
Marker |
マーカーの表示 |
InfoWindow |
情報ウィンドウ |
Autocomplete |
場所検索 |
DirectionsService |
ルート計算 |
DirectionsRenderer |
ルート表示 |
HeatmapLayer |
ヒートマップ |
MarkerClusterer |
マーカークラスタリング |
StreetViewPanorama |
ストリートビュー |
DrawingManager |
描画ツール |