Google Maps Integration
Complete guide to integrating Google Maps JavaScript API in Next.js applications with React components and TypeScript support.
Introduction to Google Maps JavaScript API
The Google Maps JavaScript API is the industry-leading mapping platform, powering millions of websites worldwide. With comprehensive global coverage, rich ecosystem of services, and familiar user experience, Google Maps provides unmatched capabilities for location-based applications.
Key Features
Comprehensive Data
- Global coverage with detailed mapping data
- Points of Interest (POI) information
- Traffic data and real-time conditions
- Business listings and reviews
- Street View imagery
Rich Ecosystem
- Places API for location search
- Geocoding and reverse geocoding
- Directions API for routing
- Distance Matrix API
- Elevation API
Familiar Experience
- Interface users already know
- Consistent across platforms
- Mobile-optimized controls
- Accessibility features built-in
Advanced Capabilities
- Custom map styling
- 3D buildings and terrain
- Drawing tools
- Heatmaps and overlays
- Data visualization layers
Getting Started
Obtaining an API Key
- Go to Google Cloud Console
- Create a new project or select existing one
- Enable Google Maps JavaScript API
- Create credentials (API Key)
- Restrict API key to your domain(s)
Environment Setup
# .env.local
NEXT_PUBLIC_GOOGLE_MAPS_KEY=your_google_maps_api_key_here
API Key Restrictions (Recommended)
- HTTP referrer restrictions
- API restrictions (only enable needed APIs)
- Usage quotas and alerts
Installation
Using @react-google-maps/api
This is the recommended library for React integration:
yarn add @react-google-maps/api
For TypeScript support (types are included):
yarn add @react-google-maps/api @types/google.maps
Alternative: @googlemaps/react-wrapper
Official Google wrapper (more lightweight):
yarn add @googlemaps/react-wrapper
Basic Setup
Simple Map Component
'use client';
import { useLoadScript, GoogleMap } from '@react-google-maps/api';
import { useMemo } from 'react';
const mapContainerStyle = {
width: '100%',
height: '600px'
};
const center = {
lat: 40.7128,
lng: -74.0060
};
export default function SimpleMap() {
const { isLoaded, loadError } = useLoadScript({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY || '',
});
const options = useMemo<google.maps.MapOptions>(
() => ({
disableDefaultUI: false,
clickableIcons: true,
scrollwheel: true,
}),
[]
);
if (loadError) {
return <div>Error loading maps</div>;
}
if (!isLoaded) {
return <div>Loading maps...</div>;
}
return (
<GoogleMap
mapContainerStyle={mapContainerStyle}
center={center}
zoom={12}
options={options}
/>
);
}
Controlled Map State
'use client';
import { useState, useCallback } from 'react';
import { useLoadScript, GoogleMap } from '@react-google-maps/api';
interface MapState {
center: google.maps.LatLngLiteral;
zoom: number;
}
export default function ControlledMap() {
const [mapState, setMapState] = useState<MapState>({
center: { lat: 40.7128, lng: -74.0060 },
zoom: 12
});
const { isLoaded } = useLoadScript({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY || '',
});
const onLoad = useCallback((map: google.maps.Map) => {
console.log('Map loaded:', map);
}, []);
const onCenterChanged = useCallback(() => {
// Called when map center changes
}, []);
const onZoomChanged = useCallback(() => {
// Called when zoom level changes
}, []);
if (!isLoaded) return <div>Loading...</div>;
return (
<div>
<div style={{ marginBottom: '10px' }}>
<p>Center: {mapState.center.lat.toFixed(4)}, {mapState.center.lng.toFixed(4)}</p>
<p>Zoom: {mapState.zoom}</p>
</div>
<GoogleMap
mapContainerStyle={{ width: '100%', height: '600px' }}
center={mapState.center}
zoom={mapState.zoom}
onLoad={onLoad}
onCenterChanged={onCenterChanged}
onZoomChanged={onZoomChanged}
/>
</div>
);
}
Adding Markers
Basic Markers
'use client';
import { useLoadScript, GoogleMap, Marker } from '@react-google-maps/api';
interface Location {
id: number;
name: string;
position: google.maps.LatLngLiteral;
}
const locations: Location[] = [
{ id: 1, name: 'Empire State Building', position: { lat: 40.7484, lng: -73.9857 } },
{ id: 2, name: 'Central Park', position: { lat: 40.7829, lng: -73.9654 } },
{ id: 3, name: 'Times Square', position: { lat: 40.7580, lng: -73.9855 } },
];
export default function MapWithMarkers() {
const { isLoaded } = useLoadScript({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY || '',
});
if (!isLoaded) return <div>Loading...</div>;
return (
<GoogleMap
mapContainerStyle={{ width: '100%', height: '600px' }}
center={{ lat: 40.7484, lng: -73.9857 }}
zoom={12}
>
{locations.map(location => (
<Marker
key={location.id}
position={location.position}
title={location.name}
/>
))}
</GoogleMap>
);
}
Interactive Markers with Info Windows
'use client';
import { useState } from 'react';
import { useLoadScript, GoogleMap, Marker, InfoWindow } from '@react-google-maps/api';
interface Location {
id: number;
name: string;
description: string;
position: google.maps.LatLngLiteral;
}
const locations: Location[] = [
{
id: 1,
name: 'Empire State Building',
description: 'Iconic 102-story Art Deco skyscraper',
position: { lat: 40.7484, lng: -73.9857 }
},
{
id: 2,
name: 'Central Park',
description: 'Urban park in Manhattan, NYC',
position: { lat: 40.7829, lng: -73.9654 }
},
{
id: 3,
name: 'Times Square',
description: 'Major commercial intersection and tourist destination',
position: { lat: 40.7580, lng: -73.9855 }
},
];
export default function InteractiveMarkers() {
const [selectedLocation, setSelectedLocation] = useState<Location | null>(null);
const { isLoaded } = useLoadScript({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY || '',
});
if (!isLoaded) return <div>Loading...</div>;
return (
<GoogleMap
mapContainerStyle={{ width: '100%', height: '600px' }}
center={{ lat: 40.7580, lng: -73.9755 }}
zoom={ 12}
onClick={() => setSelectedLocation(null)}
>
{locations.map(location => (
<Marker
key={location.id}
position={location.position}
onClick={() => setSelectedLocation(location)}
/>
))}
{selectedLocation && (
<InfoWindow
position={selectedLocation.position}
onCloseClick={() => setSelectedLocation(null)}
>
<div style={{ maxWidth: '200px' }}>
<h3 style={{ margin: '0 0 8px 0' }}>{selectedLocation.name}</h3>
<p style={{ margin: 0 }}>{selectedLocation.description}</p>
</div>
</InfoWindow>
)}
</GoogleMap>
);
}
Custom Marker Icons
'use client';
import { useLoadScript, GoogleMap, Marker } from '@react-google-maps/api';
interface CustomLocation {
id: number;
name: string;
type: 'restaurant' | 'hotel' | 'attraction';
position: google.maps.LatLngLiteral;
}
const locations: CustomLocation[] = [
{ id: 1, name: 'Italian Restaurant', type: 'restaurant', position: { lat: 40.7484, lng: -73.9857 } },
{ id: 2, name: 'Grand Hotel', type: 'hotel', position: { lat: 40.7829, lng: -73.9654 } },
{ id: 3, name: 'Museum', type: 'attraction', position: { lat: 40.7580, lng: -73.9855 } },
];
const markerIcons: Record<string, google.maps.Icon> = {
restaurant: {
url: '/icons/restaurant-marker.png',
scaledSize: new google.maps.Size(40, 40),
},
hotel: {
url: '/icons/hotel-marker.png',
scaledSize: new google.maps.Size(40, 40),
},
attraction: {
url: '/icons/attraction-marker.png',
scaledSize: new google.maps.Size(40, 40),
},
};
export default function CustomMarkers() {
const { isLoaded } = useLoadScript({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY || '',
});
if (!isLoaded) return <div>Loading...</div>;
return (
<GoogleMap
mapContainerStyle={{ width: '100%', height: '600px' }}
center={{ lat: 40.7631, lng: -73.9789 }}
zoom: 12
>
{locations.map(location => (
<Marker
key={location.id}
position={location.position}
icon={markerIcons[location.type]}
title={location.name}
/>
))}
</GoogleMap>
);
}
Drawing Shapes
Polylines (Routes)
'use client';
import { useLoadScript, GoogleMap, Polyline } from '@react-google-maps/api';
const routePath: google.maps.LatLngLiteral[] = [
{ lat: 40.7484, lng: -73.9857 },
{ lat: 40.7589, lng: -73.9851 },
{ lat: 40.7614, lng: -73.9776 },
{ lat: 40.7580, lng: -73.9855 },
];
const polylineOptions: google.maps.PolylineOptions = {
strokeColor: '#0088FF',
strokeOpacity: 0.8,
strokeWeight: 4,
geodesic: true,
};
export default function RouteMap() {
const { isLoaded } = useLoadScript({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY || '',
});
if (!isLoaded) return <div>Loading...</div>;
return (
<GoogleMap
mapContainerStyle={{ width: '100%', height: '600px' }}
center={{ lat: 40.7580, lng: -73.9800 }}
zoom: 13
>
<Polyline
path={routePath}
options={polylineOptions}
/>
</GoogleMap>
);
}
Polygons (Areas)
'use client';
import { useLoadScript, GoogleMap, Polygon } from '@react-google-maps/api';
const areaPath: google.maps.LatLngLiteral[] = [
{ lat: 40.7484, lng: -73.9857 },
{ lat: 40.7589, lng: -73.9851 },
{ lat: 40.7614, lng: -73.9776 },
{ lat: 40.7580, lng: -73.9855 },
];
const polygonOptions: google.maps.PolygonOptions = {
fillColor: '#0088FF',
fillOpacity: 0.35,
strokeColor: '#0088FF',
strokeOpacity: 0.8,
strokeWeight: 2,
};
export default function AreaMap() {
const { isLoaded } = useLoadScript({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY || '',
});
if (!isLoaded) return <div>Loading...</div>;
return (
<GoogleMap
mapContainerStyle={{ width: '100%', height: '600px' }}
center={{ lat: 40.7580, lng: -73.9800 }}
zoom: 13
>
<Polygon
paths={areaPath}
options={polygonOptions}
/>
</GoogleMap>
);
}
Circles
'use client';
import { useLoadScript, GoogleMap, Circle } from '@react-google-maps/api';
const circleOptions: google.maps.CircleOptions = {
strokeColor: '#FF0000',
strokeOpacity: 0.8,
strokeWeight: 2,
fillColor: '#FF0000',
fillOpacity: 0.35,
radius: 1000, // meters
};
export default function CircleMap() {
const { isLoaded } = useLoadScript({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY || '',
});
if (!isLoaded) return <div>Loading...</div>;
return (
<GoogleMap
mapContainerStyle={{ width: '100%', height: '600px' }}
center={{ lat: 40.7580, lng: -73.9855 }}
zoom: 13
>
<Circle
center={{ lat: 40.7580, lng: -73.9855 }}
options={circleOptions}
/>
</GoogleMap>
);
}
Geocoding
Address to Coordinates
'use client';
import { useState } from 'react';
import { useLoadScript, GoogleMap, Marker } from '@react-google-maps/api';
export default function GeocodingMap() {
const [address, setAddress] = useState('');
const [result, setResult] = useState<google.maps.LatLngLiteral | null>(null);
const [error, setError] = useState<string>('');
const { isLoaded } = useLoadScript({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY || '',
});
const geocodeAddress = async () => {
if (!address || !isLoaded) return;
const geocoder = new google.maps.Geocoder();
try {
const response = await geocoder.geocode({ address });
if (response.results[0]) {
const location = response.results[0].geometry.location;
setResult({
lat: location.lat(),
lng: location.lng()
});
setError('');
} else {
setError('No results found');
}
} catch (err) {
setError('Geocoding failed');
console.error(err);
}
};
if (!isLoaded) return <div>Loading...</div>;
return (
<div>
<div style={{ marginBottom: '10px', display: 'flex', gap: '10px' }}>
<input
type="text"
value={address}
onChange={e => setAddress(e.target.value)}
onKeyPress={e => e.key === 'Enter' && geocodeAddress()}
placeholder="Enter an address..."
style={{
flex: 1,
padding: '8px',
border: '1px solid #ccc',
borderRadius: '4px'
}}
/>
<button onClick={geocodeAddress}>Search</button>
</div>
{error && (
<div style={{ color: 'red', marginBottom: '10px' }}>{error}</div>
)}
{result && (
<div style={{ marginBottom: '10px' }}>
Found: {result.lat.toFixed(4)}, {result.lng.toFixed(4)}
</div>
)}
<GoogleMap
mapContainerStyle={{ width: '100%', height: '600px' }}
center={result || { lat: 40.7128, lng: -74.0060 }}
zoom={result ? 15 : 12}
>
{result && <Marker position={result} />}
</GoogleMap>
</div>
);
}
Reverse Geocoding
'use client';
import { useState } from 'react';
import { useLoadScript, GoogleMap, Marker } from '@react-google-maps/api';
export default function ReverseGeocodingMap() {
const [clickedPosition, setClickedPosition] = useState<google.maps.LatLngLiteral | null>(null);
const [address, setAddress] = useState<string>('');
const { isLoaded } = useLoadScript({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY || '',
});
const handleMapClick = async (e: google.maps.MapMouseEvent) => {
if (!e.latLng) return;
const position = {
lat: e.latLng.lat(),
lng: e.latLng.lng()
};
setClickedPosition(position);
// Reverse geocode
const geocoder = new google.maps.Geocoder();
try {
const response = await geocoder.geocode({ location: position });
if (response.results[0]) {
setAddress(response.results[0].formatted_address);
}
} catch (err) {
console.error('Reverse geocoding failed:', err);
}
};
if (!isLoaded) return <div>Loading...</div>;
return (
<div>
<div style={{ marginBottom: '10px' }}>
<p><strong>Click on the map to get the address</strong></p>
{address && (
<div style={{
padding: '10px',
backgroundColor: '#f5f5f5',
borderRadius: '4px'
}}>
<strong>Address:</strong> {address}
</div>
)}
</div>
<GoogleMap
mapContainerStyle={{ width: '100%', height: '600px' }}
center={{ lat: 40.7128, lng: -74.0060 }}
zoom={12}
onClick={handleMapClick}
>
{clickedPosition && <Marker position={clickedPosition} />}
</GoogleMap>
</div>
);
}
Places API Integration
'use client';
import { useState, useCallback } from 'react';
import { useLoadScript, GoogleMap, Marker } from '@react-google-maps/api';
const libraries: ('places')[] = ['places'];
interface Place {
name: string;
position: google.maps.LatLngLiteral;
placeId: string;
}
export default function PlacesSearchMap() {
const [places, setPlaces] = useState<Place[]>([]);
const [map, setMap] = useState<google.maps.Map | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const { isLoaded } = useLoadScript({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY || '',
libraries,
});
const onLoad = useCallback((mapInstance: google.maps.Map) => {
setMap(mapInstance);
}, []);
const searchPlaces = () => {
if (!map || !searchQuery) return;
const service = new google.maps.places.PlacesService(map);
const center = map.getCenter();
if (!center) return;
const request: google.maps.places.TextSearchRequest = {
query: searchQuery,
location: center,
radius: 5000,
};
service.textSearch(request, (results, status) => {
if (status === google.maps.places.PlacesServiceStatus.OK && results) {
const newPlaces: Place[] = results.slice(0, 10).map(result => ({
name: result.name || 'Unknown',
position: {
lat: result.geometry?.location?.lat() || 0,
lng: result.geometry?.location?.lng() || 0,
},
placeId: result.place_id || '',
}));
setPlaces(newPlaces);
}
});
};
if (!isLoaded) return <div>Loading...</div>;
return (
<div>
<div style={{ marginBottom: '10px', display: 'flex', gap: '10px' }}>
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
onKeyPress={e => e.key === 'Enter' && searchPlaces()}
placeholder="Search for places (e.g., 'restaurants near me')"
style={{
flex: 1,
padding: '8px',
border: '1px solid #ccc',
borderRadius: '4px'
}}
/>
<button onClick={searchPlaces}>Search</button>
</div>
{places.length > 0 && (
<div style={{ marginBottom: '10px' }}>
Found {places.length} places
</div>
)}
<GoogleMap
mapContainerStyle={{ width: '100%', height: '600px' }}
center={{ lat: 40.7128, lng: -74.0060 }}
zoom={ 13}
onLoad={onLoad}
>
{places.map(place => (
<Marker
key={place.placeId}
position={place.position}
title={place.name}
/>
))}
</GoogleMap>
</div>
);
}
Directions API
'use client';
import { useState, useCallback } from 'react';
import { useLoadScript, GoogleMap, DirectionsRenderer } from '@react-google-maps/api';
export default function DirectionsMap() {
const [directions, setDirections] = useState<google.maps.DirectionsResult | null>(null);
const [origin, setOrigin] = useState('');
const [destination, setDestination] = useState('');
const [error, setError] = useState('');
const { isLoaded } = useLoadScript({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY || '',
});
const calculateRoute = useCallback(async () => {
if (!origin || !destination) {
setError('Please enter both origin and destination');
return;
}
const directionsService = new google.maps.DirectionsService();
try {
const result = await directionsService.route({
origin,
destination,
travelMode: google.maps.TravelMode.DRIVING,
});
setDirections(result);
setError('');
} catch (err) {
setError('Failed to calculate route');
console.error(err);
}
}, [origin, destination]);
if (!isLoaded) return <div>Loading...</div>;
return (
<div>
<div style={{ marginBottom: '10px' }}>
<div style={{ display: 'flex', gap: '10px', marginBottom: '10px' }}>
<input
type="text"
value={origin}
onChange={e => setOrigin(e.target.value)}
placeholder="Origin"
style={{
flex: 1,
padding: '8px',
border: '1px solid #ccc',
borderRadius: '4px'
}}
/>
<input
type="text"
value={destination}
onChange={e => setDestination(e.target.value)}
placeholder="Destination"
style={{
flex: 1,
padding: '8px',
border: '1px solid #ccc',
borderRadius: '4px'
}}
/>
<button onClick={calculateRoute}>Get Directions</button>
</div>
{error && <div style={{ color: 'red' }}>{error}</div>}
{directions && (
<div style={{
padding: '10px',
backgroundColor: '#f5f5f5',
borderRadius: '4px'
}}>
<strong>Distance:</strong> {directions.routes[0].legs[0].distance?.text}<br />
<strong>Duration:</strong> {directions.routes[0].legs[0].duration?.text}
</div>
)}
</div>
<GoogleMap
mapContainerStyle={{ width: '100%', height: '600px' }}
center={{ lat: 40.7128, lng: -74.0060 }}
zoom={12}
>
{directions && (
<DirectionsRenderer
directions={directions}
options={{
polylineOptions: {
strokeColor: '#0088FF',
strokeWeight: 5,
},
}}
/>
)}
</GoogleMap>
</div>
);
}
Map Styling
'use client';
import { useState } from 'react';
import { useLoadScript, GoogleMap } from '@react-google-maps/api';
// Custom map styles (Silver theme example)
const silverStyle: google.maps.MapTypeStyle[] = [
{
elementType: 'geometry',
stylers: [{ color: '#f5f5f5' }],
},
{
elementType: 'labels.icon',
stylers: [{ visibility: 'off' }],
},
{
elementType: 'labels.text.fill',
stylers: [{ color: '#616161' }],
},
{
elementType: 'labels.text.stroke',
stylers: [{ color: '#f5f5f5' }],
},
{
featureType: 'water',
elementType: 'geometry',
stylers: [{ color: '#c9c9c9' }],
},
];
// Dark theme
const darkStyle: google.maps.MapTypeStyle[] = [
{ elementType: 'geometry', stylers: [{ color: '#242f3e' }] },
{ elementType: 'labels.text.stroke', stylers: [{ color: '#242f3e' }] },
{ elementType: 'labels.text.fill', stylers: [{ color: '#746855' }] },
{
featureType: 'water',
elementType: 'geometry',
stylers: [{ color: '#17263c' }],
},
];
const styles = {
default: [],
silver: silverStyle,
dark: darkStyle,
};
export default function StyledMap() {
const [currentStyle, setCurrentStyle] = useState<keyof typeof styles>('default');
const { isLoaded } = useLoadScript({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY || '',
});
if (!isLoaded) return <div>Loading...</div>;
return (
<div>
<div style={{ marginBottom: '10px', display: 'flex', gap: '5px' }}>
{Object.keys(styles).map(style => (
<button
key={style}
onClick={() => setCurrentStyle(style as keyof typeof styles)}
style={{
padding: '8px 16px',
backgroundColor: currentStyle === style ? '#0080ff' : '#f5f5f5',
color: currentStyle === style ? 'white' : 'black',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
textTransform: 'capitalize'
}}
>
{style}
</button>
))}
</div>
<GoogleMap
mapContainerStyle={{ width: '100%', height: '600px' }}
center={{ lat: 40.7128, lng: -74.0060 }}
zoom={12}
options={{
styles: styles[currentStyle],
}}
/>
</div>
);
}
Heatmap Layer
'use client';
import { useMemo } from 'react';
import { useLoadScript, GoogleMap } from '@react-google-maps/api';
const libraries: ('visualization')[] = ['visualization'];
export default function HeatmapExample() {
const { isLoaded } = useLoadScript({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY || '',
libraries,
});
const heatmapData = useMemo(() => {
// Generate random points
return Array.from({ length: 500 }, () => {
return new google.maps.LatLng(
40.7128 + (Math.random() - 0.5) * 0.1,
-74.0060 + (Math.random() - 0.5) * 0.1
);
});
}, []);
const onLoad = (map: google.maps.Map) => {
const heatmap = new google.maps.visualization.HeatmapLayer({
data: heatmapData,
map: map,
});
heatmap.setOptions({
radius: 20,
opacity: 0.6,
});
};
if (!isLoaded) return <div>Loading...</div>;
return (
<GoogleMap
mapContainerStyle={{ width: '100%', height: '600px' }}
center={{ lat: 40.7128, lng: -74.0060 }}
zoom={ 13}
onLoad={onLoad}
/>
);
}
Performance Optimization
Memoization and useCallback
'use client';
import { useState, useMemo, useCallback } from 'react';
import { useLoadScript, GoogleMap, Marker } from '@react-google-maps/api';
export default function OptimizedMap({ locations }: { locations: any[] }) {
const [selectedId, setSelectedId] = useState<number | null>(null);
const { isLoaded } = useLoadScript({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY || '',
});
// Memoize map options
const mapOptions = useMemo<google.maps.MapOptions>(
() => ({
disableDefaultUI: false,
clickableIcons: false,
scrollwheel: true,
}),
[]
);
// Memoize marker click handler
const handleMarkerClick = useCallback((id: number) => {
setSelectedId(id);
}, []);
// Memoize map center
const center = useMemo(() => {
if (locations.length === 0) {
return { lat: 40.7128, lng: -74.0060 };
}
return {
lat: locations[0].lat,
lng: locations[0].lng,
};
}, [locations]);
if (!isLoaded) return <div>Loading...</div>;
return (
<GoogleMap
mapContainerStyle={{ width: '100%', height: '600px' }}
center={center}
zoom={12}
options={mapOptions}
>
{locations.map(location => (
<Marker
key={location.id}
position={{ lat: location.lat, lng: location.lng }}
onClick={() => handleMarkerClick(location.id)}
/>
))}
</GoogleMap>
);
}
Lazy Loading
// app/map-page/page.tsx
'use client';
import dynamic from 'next/dynamic';
const GoogleMapComponent = dynamic(
() => import('@/components/GoogleMapComponent'),
{
ssr: false,
loading: () => (
<div style={{
width: '100%',
height: '600px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#f5f5f5'
}}>
Loading Google Maps...
</div>
)
}
);
export default function MapPage() {
return (
<div>
<h1>Google Maps Integration</h1>
<GoogleMapComponent />
</div>
);
}
TypeScript Best Practices
// types/google-maps.ts
export interface MapLocation {
id: number;
name: string;
position: google.maps.LatLngLiteral;
type: 'restaurant' | 'hotel' | 'attraction';
}
export interface MapComponentProps {
locations: MapLocation[];
center?: google.maps.LatLngLiteral;
zoom?: number;
onLocationClick?: (location: MapLocation) => void;
}
// components/TypedGoogleMap.tsx
'use client';
import { useCallback } from 'react';
import { useLoadScript, GoogleMap, Marker } from '@react-google-maps/api';
import type { MapComponentProps } from '@/types/google-maps';
export default function TypedGoogleMap({
locations,
center = { lat: 40.7128, lng: -74.0060 },
zoom = 12,
onLocationClick
}: MapComponentProps) {
const { isLoaded, loadError } = useLoadScript({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY || '',
});
const handleMarkerClick = useCallback((location: MapLocation) => {
if (onLocationClick) {
onLocationClick(location);
}
}, [onLocationClick]);
if (loadError) {
return <div>Error loading maps: {loadError.message}</div>;
}
if (!isLoaded) {
return <div>Loading maps...</div>;
}
return (
<GoogleMap
mapContainerStyle={{ width: '100%', height: '600px' }}
center={center}
zoom={zoom}
>
{locations.map(location => (
<Marker
key={location.id}
position={location.position}
onClick={() => handleMarkerClick(location)}
title={location.name}
/>
))}
</GoogleMap>
);
}
Best Practices
1. Secure API Key Management
NEXT_PUBLIC_GOOGLE_MAPS_KEY=your_api_key
2. Restrict API Keys
- Add HTTP referrer restrictions
- Enable only required APIs
- Set usage quotas
3. Lazy Load Maps
const Map = dynamic(() => import('./Map'), { ssr: false });
4. Memoize Options
const options = useMemo(() => ({ /* options */ }), []);
5. Handle Loading States
if (!isLoaded) return <div>Loading...</div>;
if (loadError) return <div>Error loading maps</div>;
6. Use useCallback for Event Handlers
const handleClick = useCallback(() => { /* ... */ }, [deps]);
7. Monitor API Usage
- Set up billing alerts
- Track API calls in Cloud Console
- Optimize marker clustering for large datasets
Google Maps provides the most comprehensive mapping solution with unmatched global coverage, rich services ecosystem, and familiar user experience, making it ideal for location-based applications requiring high-quality data and advanced features.