Use Maptoolkit Maps in React
A map in React is a useRef for the container, a useRef for the map instance and one
useEffect that creates and destroys it. The map is not state, it is not a prop, and it never
belongs in useState.
The component
import { useEffect, useRef } from "react";
import { Map } from "@maptoolkit/maps";
import "@maptoolkit/maps/css";
export default function MaptoolkitMap() {
const containerRef = useRef(null);
const mapRef = useRef(null);
useEffect(() => {
if (mapRef.current) return;
mapRef.current = new Map({
container: containerRef.current,
apiKey: process.env.REACT_APP_MAPTOOLKIT_KEY,
center: [16.37, 48.21],
zoom: 12,
});
return () => {
mapRef.current?.remove();
mapRef.current = null;
};
}, []);
return <div ref={containerRef} style={{ width: "100%", height: "400px" }} />;
}Four details in that snippet matter more than they look.
The container gets a height. A div with no height is zero pixels tall, the map
initializes into nothing and you get a blank page with no error. This is the single most
common “the map does not show up” report, and it is never the map’s fault.
The effect’s dependency array is empty. Anything in that array re-runs the effect, which tears down and rebuilds the map. A map rebuilt on every render loses the user’s position on every keystroke elsewhere in the form.
The cleanup calls remove(). Without it, navigating away from the page leaks the WebGL
context. Browsers cap the number of live contexts at around 8 to 16; past the cap, existing
maps go blank rather than throwing.
The map is in a ref, not state. Putting it in useState triggers a re-render on every
update and invites React into an object it should not be diffing.
StrictMode creates two maps
In development, React 18 and later deliberately mount, unmount and remount every component
once, to surface missing cleanup. The effect above therefore runs twice, and without the
if (mapRef.current) return; guard you get two maps stacked in one container: doubled
controls, doubled attribution and events firing twice.
It only happens in development, which makes it worse rather than better, because the guard that fixes it is easy to leave out and the symptom never reaches production for you to notice. The guard plus a cleanup that nulls the ref handles both the StrictMode remount and a real unmount.
Changing the map after it exists
Once the map is created, drive it with imperative calls from effects that depend on the values you care about. Do not recreate it.
useEffect(() => {
mapRef.current?.flyTo({ center: selected.coords, zoom: 14 });
}, [selected]);The same applies to markers, sources and layers: add them in a load handler or in an effect
that runs after creation, and remove them in that effect’s cleanup.
useEffect(() => {
const map = mapRef.current;
if (!map) return;
const marker = new Marker().setLngLat(selected.coords).addTo(map);
return () => marker.remove();
}, [selected]);Waiting for the style
Anything that touches sources or layers has to wait for the style to load, or it throws “Style is not done loading”. Inside a component that means guarding on the event rather than assuming:
map.on("load", () => {
map.addSource("route", { type: "geojson", data: routeGeoJSON });
map.addLayer({ id: "route", type: "line", source: "route" });
});If the map may already be loaded by the time your effect runs, check map.isStyleLoaded()
first and skip the listener.
Where to put the API key
Bundlers inline environment variables at build time, so anything you reference ends up in the JavaScript the browser downloads. A map key is a public credential by nature, since the browser has to send it, so this is expected rather than a leak, but it means the key is readable by anyone who opens the network tab. Restrict it to your domains rather than relying on it being hidden.
Next steps
- Using Maptoolkit maps in Next.js if you are server rendering, which needs one more step.
- Maps JS API Reference for the classes used above.
- Maps JS Examples for what to do with the map once it is mounted.