Advanced
Access the underlying AMap JS API instance for advanced customization.
Access the underlying AMap JS API map instance to use any feature from the AMap JS API JS API. You can use either a ref or the useMap hook.
Using a Ref
The simplest way to access the map instance. Use a ref to call map methods from event handlers or effects.
import { Map, type MapRef } from "@/components/ui/map";
import { useRef } from "react";
function MyMapComponent() {
const mapRef = useRef<MapRef>(null);
const handleFlyTo = () => {
// Access the AMap JS API map instance via ref
mapRef.current?.flyTo({ center: [-74, 40.7], zoom: 12 });
};
return (
<>
<button onClick={handleFlyTo}>Fly to NYC</button>
<Map ref={mapRef} center={[-74, 40.7]} zoom={10} />
</>
);
}Using the Hook
For child components rendered inside Map, use the useMap hook to access the map instance and listen to events.
import { Map, useMap } from "@/components/ui/map";
import { useEffect } from "react";
// For child components inside Map, use the useMap hook
function MapEventListener() {
const { map, isLoaded } = useMap();
useEffect(() => {
if (!map || !isLoaded) return;
const handleClick = (e) => {
console.log("Clicked at:", e.lngLat);
};
map.on("click", handleClick);
return () => map.off("click", handleClick);
}, [map, isLoaded]);
return null;
}
// Usage
<Map center={[-74, 40.7]} zoom={10}>
<MapEventListener />
</Map>useMapEvent
useMapEvent(event, handler) is a convenience hook that subscribes to any AMap map event and automatically unsubscribes on unmount. It eliminates the boilerplate of writing useMap + useEffect + map.on/off yourself. The hook must be called from a component rendered inside Map.
"use client";
import { useState } from "react";
import { Map, MapMarker, MarkerContent, useMapEvent } from "@/components/ui/map";
function ClickListener({
onMapClick,
}: {
onMapClick: (point: { lng: number; lat: number }) => void;
}) {
useMapEvent("click", (e: { lnglat: { getLng: () => number; getLat: () => number } }) => {
onMapClick({ lng: e.lnglat.getLng(), lat: e.lnglat.getLat() });
});
return null;
}
export function UseMapEventExample() {
const [lastClick, setLastClick] = useState<{
lng: number;
lat: number;
} | null>(null);
return (
<div className="h-[400px] w-full relative">
<Map center={[116.397428, 39.90923]} zoom={11}>
<ClickListener onMapClick={setLastClick} />
{lastClick && (
<MapMarker longitude={lastClick.lng} latitude={lastClick.lat}>
<MarkerContent>
<div className="size-3 rounded-full bg-rose-500 border-2 border-white shadow-lg" />
</MarkerContent>
</MapMarker>
)}
</Map>
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 z-10 bg-background/90 backdrop-blur border rounded-md px-4 py-2 text-xs text-center">
{lastClick
? `Clicked: ${lastClick.lat.toFixed(4)}, ${lastClick.lng.toFixed(4)}`
: "Click anywhere on the map"}
</div>
</div>
);
}
useMapBounds
useMapBounds() returns the current viewport bounding box as { north, south, east, west } (or null before the map loads). The value updates on every pan and zoom. Useful for fetching data within the visible area.
"use client";
import { Map } from "@/components/ui/map";
import { useMapBounds } from "@/components/ui/map";
function BoundsDisplay() {
const bounds = useMapBounds();
if (!bounds) {
return null;
}
return (
<div className="absolute bottom-3 left-3 right-3 z-10 bg-background/90 backdrop-blur border rounded-md px-3 py-2 font-mono text-[10px] grid grid-cols-2 gap-x-4 gap-y-0.5">
<span className="text-muted-foreground">North:</span>
<span>{bounds.north.toFixed(4)}</span>
<span className="text-muted-foreground">South:</span>
<span>{bounds.south.toFixed(4)}</span>
<span className="text-muted-foreground">East:</span>
<span>{bounds.east.toFixed(4)}</span>
<span className="text-muted-foreground">West:</span>
<span>{bounds.west.toFixed(4)}</span>
</div>
);
}
export function UseMapBoundsExample() {
return (
<div className="h-[400px] w-full relative">
<Map center={[116.397428, 39.90923]} zoom={11}>
<BoundsDisplay />
</Map>
<div className="absolute top-3 left-3 z-10 bg-background/90 backdrop-blur border rounded-md px-3 py-2 text-xs text-muted-foreground">
Pan or zoom to update bounds
</div>
</div>
);
}
Example: Custom Controls
This example shows how to create custom controls that manipulate the map's pitch and bearing, and listen to map events to display real-time values.
"use client";
import { useEffect, useState } from "react";
import { Map, useMap } from "@/components/ui/map";
import { Button } from "@/components/ui/button";
import { RotateCcw, Mountain } from "lucide-react";
function MapController() {
const { map, isLoaded } = useMap();
const [pitch, setPitch] = useState(0);
const [bearing, setBearing] = useState(0);
useEffect(() => {
if (!map || !isLoaded) return;
const handleMove = () => {
setPitch(Math.round(map.getPitch?.() ?? 0));
setBearing(Math.round(map.getRotation?.() ?? 0));
};
map.on("moveend", handleMove);
map.on("pitchchange", handleMove);
map.on("rotatechange", handleMove);
return () => {
map.off("moveend", handleMove);
map.off("pitchchange", handleMove);
map.off("rotatechange", handleMove);
};
}, [map, isLoaded]);
const handle3DView = () => {
map?.setPitch(60);
map?.setRotation(-20);
};
const handleReset = () => {
map?.setPitch(0);
map?.setRotation(0);
};
if (!isLoaded) return null;
return (
<div className="absolute top-3 left-3 z-10 flex flex-col gap-2">
<div className="flex gap-2">
<Button size="sm" variant="secondary" onClick={handle3DView}>
<Mountain className="size-4 mr-1.5" />
3D View
</Button>
<Button size="sm" variant="secondary" onClick={handleReset}>
<RotateCcw className="size-4 mr-1.5" />
Reset
</Button>
</div>
<div className="rounded-md bg-background/90 backdrop-blur px-3 py-2 text-xs font-mono border">
<div>Pitch: {pitch}°</div>
<div>Bearing: {bearing}°</div>
</div>
</div>
);
}
export function AdvancedUsageExample() {
return (
<div className="h-[400px] w-full">
<Map center={[116.397428, 39.90923]} zoom={14}>
<MapController />
</Map>
</div>
);
}
Example: Custom GeoJSON Layer
Add custom GeoJSON data as layers with fill and outline styles. This example shows NYC parks with hover interactions.
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { Map, MapControls, useMap } from "@/components/ui/map";
import { Button } from "@/components/ui/button";
import { Layers, X } from "lucide-react";
// Forbidden City (故宫博物院) approximate boundary
const forbiddenCity = {
name: "故宫博物院",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
path: [
[116.3913, 39.9134],
[116.4030, 39.9134],
[116.4030, 39.9243],
[116.3913, 39.9243],
[116.3913, 39.9134],
] as [number, number][],
};
function CustomLayer() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const polygonsRef = useRef<any[]>([]);
const { map, AMap, isLoaded } = useMap();
const [isLayerVisible, setIsLayerVisible] = useState(false);
const [hoveredPark, setHoveredPark] = useState<string | null>(null);
const removeLayers = useCallback(() => {
polygonsRef.current.forEach((p) => p.setMap(null));
polygonsRef.current = [];
}, []);
const addLayers = useCallback(() => {
if (!map || !AMap) return;
removeLayers();
const polygon = new AMap.Polygon({
path: forbiddenCity.path,
fillColor: "#ef4444",
fillOpacity: 0.25,
strokeColor: "#dc2626",
strokeWeight: 2,
strokeOpacity: 1,
});
polygon.setMap(map);
polygon.on("mouseover", () => setHoveredPark(forbiddenCity.name));
polygon.on("mouseout", () => setHoveredPark(null));
polygonsRef.current.push(polygon);
}, [map, AMap, removeLayers]);
useEffect(() => {
if (!isLoaded) return;
return () => removeLayers();
}, [isLoaded, removeLayers]);
const toggleLayer = () => {
if (isLayerVisible) {
removeLayers();
setIsLayerVisible(false);
} else {
addLayers();
setIsLayerVisible(true);
}
};
return (
<>
<div className="absolute top-3 left-3 z-10">
<Button
size="sm"
variant={isLayerVisible ? "default" : "secondary"}
onClick={toggleLayer}
>
{isLayerVisible ? (
<X className="size-4 mr-1.5" />
) : (
<Layers className="size-4 mr-1.5" />
)}
{isLayerVisible ? "Hide Layer" : "Show Layer"}
</Button>
</div>
{hoveredPark && (
<div className="absolute bottom-3 left-3 z-10 rounded-md bg-background/90 backdrop-blur px-3 py-2 text-sm font-medium border">
{hoveredPark}
</div>
)}
</>
);
}
export function CustomLayerExample() {
return (
<div className="h-[400px] w-full">
<Map center={[116.3972, 39.9189]} zoom={14}>
<MapControls />
<CustomLayer />
</Map>
</div>
);
}
Example: Markers via Layers
When displaying hundreds or thousands of markers, use GeoJSON layers instead of DOM-based MapMarker components. This approach renders markers on the WebGL canvas, providing significantly better performance.
"use client";
import { useEffect, useRef, useState } from "react";
import { Map, MapPopup, useMap } from "@/components/ui/map";
// Generate random points around Beijing
function generateRandomPoints(count: number) {
const center = { lng: 116.397428, lat: 39.90923 };
const features = [];
for (let i = 0; i < count; i++) {
const lng = center.lng + (Math.random() - 0.5) * 0.15;
const lat = center.lat + (Math.random() - 0.5) * 0.1;
features.push({
type: "Feature" as const,
properties: {
id: i,
name: `Location ${i + 1}`,
category: ["Restaurant", "Cafe", "Bar", "Shop"][
Math.floor(Math.random() * 4)
],
},
geometry: {
type: "Point" as const,
coordinates: [lng, lat],
},
});
}
return {
type: "FeatureCollection" as const,
features,
};
}
// 200 markers rendered natively via AMap
const pointsData = generateRandomPoints(200);
interface SelectedPoint {
id: number;
name: string;
category: string;
coordinates: [number, number];
}
function MarkersLayer() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const markersRef = useRef<any[]>([]);
const { map, AMap, isLoaded } = useMap();
const [selectedPoint, setSelectedPoint] = useState<SelectedPoint | null>(null);
useEffect(() => {
if (!map || !AMap || !isLoaded) return;
pointsData.features.forEach((f) => {
const [lng, lat] = f.geometry.coordinates;
const div = document.createElement("div");
div.style.cssText =
"width:12px;height:12px;border-radius:50%;background:#3b82f6;border:2px solid white;box-shadow:0 1px 4px rgba(0,0,0,.3);cursor:pointer;";
const marker = new AMap.Marker({
position: [lng, lat],
content: div,
offset: new AMap.Pixel(-6, -6),
});
marker.setMap(map);
marker.on("click", () => {
setSelectedPoint({
id: f.properties.id,
name: f.properties.name,
category: f.properties.category,
coordinates: [lng, lat],
});
});
markersRef.current.push(marker);
});
return () => {
markersRef.current.forEach((m) => m.setMap(null));
markersRef.current = [];
};
}, [map, AMap, isLoaded]);
return (
<>
{selectedPoint && (
<MapPopup
longitude={selectedPoint.coordinates[0]}
latitude={selectedPoint.coordinates[1]}
onClose={() => setSelectedPoint(null)}
closeButton
>
<div className="min-w-[140px]">
<p className="font-medium">{selectedPoint.name}</p>
<p className="text-sm text-muted-foreground">
{selectedPoint.category}
</p>
</div>
</MapPopup>
)}
</>
);
}
export function LayerMarkersExample() {
return (
<div className="h-[400px] w-full">
<Map center={[116.397428, 39.90923]} zoom={11}>
<MarkersLayer />
</Map>
</div>
);
}
Extend to Build
You can extend this to build custom features like:
- Real-time tracking - Live location updates for delivery, rides, or fleet management
- Geofencing - Trigger actions when users enter or leave specific areas
- Heatmaps - Visualize density data like population, crime, or activity hotspots
- Drawing tools - Let users draw polygons, lines, or place markers for custom areas
- 3D buildings - Extrude building footprints for urban visualization
- Animations - Animate markers along routes or create fly-through experiences
- Custom data layers - Overlay weather, traffic, or satellite imagery