Advanced
Use the underlying MapLibre instance for custom sources, layers, and interactions.
The useMap hook gives child components access to the MapLibre instance after the map has loaded. Use it when you need MapLibre APIs directly, such as custom sources, custom layers, hover queries, visibility toggles, heatmaps, or raster overlays.
Custom GeoJSON Layer
Installation
Usage
Create a child component that reads { map, isLoaded } from useMap, then add your source and layers inside an effect. Always guard on isLoaded, keep source and layer ids stable, and remove layers before removing the source during cleanup.
import { useEffect } from "react";
import { Map, useMap } from "@notion-kit/map";
const geojsonData = {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: { name: "Daan Forest Park" },
geometry: {
type: "Polygon",
coordinates: [
[
[121.5321, 25.0347],
[121.5379, 25.0345],
[121.538, 25.0291],
[121.5322, 25.0293],
[121.5321, 25.0347],
],
],
},
},
],
} satisfies GeoJSON.FeatureCollection<GeoJSON.Polygon>;
function ParksLayer() {
const { map, isLoaded } = useMap();
useEffect(() => {
if (!map || !isLoaded) return;
map.addSource("parks", {
type: "geojson",
data: geojsonData,
});
map.addLayer({
id: "parks-fill",
type: "fill",
source: "parks",
paint: {
"fill-color": "#22c55e",
"fill-opacity": 0.4,
},
});
return () => {
if (map.getLayer("parks-fill")) map.removeLayer("parks-fill");
if (map.getSource("parks")) map.removeSource("parks");
};
}, [isLoaded, map]);
return null;
}
export function Example() {
return (
<Map center={[121.525, 25.037]} zoom={13}>
<ParksLayer />
</Map>
);
}Updating GeoJSON Data
If the GeoJSON changes after the source is created, update the existing source instead of adding it again.
import type { GeoJSONSource } from "maplibre-gl";
function ParksLayer({ data }: { data: GeoJSON.FeatureCollection }) {
const { map, isLoaded } = useMap();
useEffect(() => {
if (!map || !isLoaded) return;
if (!map.getSource("parks")) {
map.addSource("parks", { type: "geojson", data });
}
if (!map.getLayer("parks-fill")) {
map.addLayer({
id: "parks-fill",
type: "fill",
source: "parks",
paint: { "fill-color": "#22c55e", "fill-opacity": 0.4 },
});
}
return () => {
if (map.getLayer("parks-fill")) map.removeLayer("parks-fill");
if (map.getSource("parks")) map.removeSource("parks");
};
}, [isLoaded, map]);
useEffect(() => {
if (!map || !isLoaded) return;
const source = map.getSource("parks") as GeoJSONSource | undefined;
source?.setData(data);
}, [data, isLoaded, map]);
return null;
}Layer Interactions
Use MapLibre layer events for hover and click states. Querying rendered features lets you read GeoJSON properties from the feature under the cursor.
useEffect(() => {
if (!map || !isLoaded || !map.getLayer("parks-fill")) return;
const handleMouseMove = (event: maplibregl.MapMouseEvent) => {
const features = map.queryRenderedFeatures(event.point, {
layers: ["parks-fill"],
});
console.log(features[0]?.properties?.name);
};
const handleMouseEnter = () => {
map.getCanvas().style.cursor = "pointer";
};
const handleMouseLeave = () => {
map.getCanvas().style.cursor = "";
};
map.on("mousemove", "parks-fill", handleMouseMove);
map.on("mouseenter", "parks-fill", handleMouseEnter);
map.on("mouseleave", "parks-fill", handleMouseLeave);
return () => {
map.off("mousemove", "parks-fill", handleMouseMove);
map.off("mouseenter", "parks-fill", handleMouseEnter);
map.off("mouseleave", "parks-fill", handleMouseLeave);
};
}, [isLoaded, map]);Visibility Toggles
Use setLayoutProperty to show or hide a layer without removing its source.
function setParksVisible(visible: boolean) {
if (!map?.getLayer("parks-fill")) return;
map.setLayoutProperty(
"parks-fill",
"visibility",
visible ? "visible" : "none",
);
}