Add a map to React
React wrappers for MapLibre are a crowded shelf, and most of them are the same idea: a declarative component tree over an imperative map. unmap does not add another one. You get the MapLibre map object itself, and the tiles, geocoding and routing behind it on one key.
On Next.js, read Add a map to Next.js instead: the client boundary changes the answer.
Install
Registry copies the component into your project, where it follows your design tokens and you
own the file. It needs a project with a components.json.
Package installs @unmap/react and gives you hooks, which is the right layer when you are
placing the map inside UI you have already built.
npx @unmap/cli add mapnpm i @unmap/react maplibre-glThe component
import { Map } from "@/components/ui/map";
export default function App() {
return (
<div className="h-dvh">
<Map
apiKey={import.meta.env.VITE_UNMAP_KEY as string}
center={[-114.07, 51.05]}
zoom={11}
className="h-full"
/>
</div>
);
}import { useUnmap } from "@unmap/react";
export default function App() {
const { mapContainer } = useUnmap({
apiKey: import.meta.env.VITE_UNMAP_KEY as string,
center: [-114.07, 51.05],
zoom: 11,
});
return <div ref={mapContainer} style={{ height: "100dvh" }} />;
}On the package path, import maplibre-gl/dist/maplibre-gl.css once in your entry file. The copied
component does that for you.
The map object is the MapLibre map
useUnmap returns the map itself, not a facade, so every MapLibre event, source, layer and control
stays available whether or not we thought to expose it. ready tells you when the load event has
fired, which is the earliest point you may add a source.
import { useEffect } from "react";
import { useUnmap } from "@unmap/react";
export default function AppWithLayer() {
const { mapContainer, map, ready } = useUnmap({
apiKey: import.meta.env.VITE_UNMAP_KEY as string,
center: [-114.07, 51.05],
zoom: 11,
});
useEffect(() => {
if (!ready || !map) return;
map.addSource("sites", { type: "geojson", data: { type: "FeatureCollection", features: [] } });
map.addLayer({ id: "sites", type: "circle", source: "sites" });
}, [ready, map]);
return <div ref={mapContainer} style={{ height: "100dvh" }} />;
}mapContainer is a callback ref rather than a ref object, so the map is created when the element
actually appears. A container rendered behind a condition or inside a tab still works.
Where the key lives
Vite inlines anything prefixed VITE_ into the browser bundle. That is correct for a map key and
worth being clear-eyed about: a key used by a map in a browser is readable by anyone who loads the
page, at every maps vendor. Restrict it to your production origins in the dashboard
rather than trying to hide it.
# .env
VITE_UNMAP_KEY=um_live_...Common problems
The map is a blank box. The container has no height. MapLibre measures the element it is given, and an element of zero pixels reports no error.
The map appears once and never again after a route change. The container was unmounted while
the hook kept the old element. useUnmap handles this with its callback ref; a hand-rolled
useRef version usually does not.
Options change and the map rebuilds. Changing center or zoom moves the existing map, but
changing anything else recreates it. Keep option objects stable, or lift them out of render.
Related
- Add a map to Next.js for the App Router version
- Components for the geocoder, routing panel, and markers
- Examples for the framework-free versions of each job