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 map

The 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>
  );
}

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.