Frameworks
A map library and a component framework disagree about who owns the DOM. The map wants a stable element it can keep for its lifetime; the framework reserves the right to unmount, re-render and re-run effects whenever it likes. Almost every bug in this section comes from that disagreement.
These pages cover the framework-specific part: where to create the map, where to destroy it, what to store it in, and what breaks during a build. The map code itself is the same as everywhere else, so use Maps JS or MapLibre GL JS for the API, and these pages for the wiring.
React
Using Maptoolkit maps in React covers the useRef and useEffect pattern,
cleaning up on unmount, and driving the map imperatively from effects instead of recreating it.
With a wrapper library, use React Leaflet for raster tiles or
react-map-gl for MapLibre GL JS.
The one that catches everybody: in development, React 18 and later mounts, unmounts and remounts every component once. Without a guard the effect runs twice and you get two maps stacked in one container, with doubled controls and events firing twice. It does not happen in production, so it never shows up where you would notice it.
Next.js
Using Maptoolkit maps in Next.js covers next/dynamic with ssr: false,
the App Router client boundary, where the stylesheet goes, and static export.
Leaflet in Next.js covers the same for React Leaflet, plus the
marker icon fix for webpack and Turbopack.
The one that catches everybody: "use client" is not enough. A client component is still
server rendered for the initial HTML, so the module is still evaluated on the server, where the
library reaches for window and throws ReferenceError: window is not defined from inside the
library rather than from your code.
Vue
Using Maptoolkit maps in Vue covers the Composition API lifecycle hooks, driving the map from watchers, and the Nuxt server-rendering case. With a wrapper library, use Vue Leaflet for raster tiles or vue-maplibre-gl for MapLibre GL JS.
The one that catches everybody: the map has to go in a shallowRef, not a ref. A plain
ref makes Vue deep-proxy the whole map instance, which ranges from a badly degraded frame
rate to the library’s own identity checks failing because it is comparing an object against a
Proxy of itself. The symptoms look like library bugs and are not.
Angular
Using Maptoolkit maps in Angular covers a standalone map component, the
stylesheet in angular.json, and @defer. With a wrapper library, use
ngx-leaflet for raster tiles or
ngx-maplibre-gl for MapLibre GL JS.
The one that catches everybody: the production build fails its size budget. A WebGL map
library is larger than Angular’s default 1 MB limit for the initial bundle, so importing it
directly breaks ng build. A @defer block around the map moves it into a lazy chunk.
The three rules that hold everywhere
- Create the map once, against a real element. Not on every render, and not before the element exists. Give that element an explicit height, or it is zero pixels tall and the map initializes into nothing with no error.
- Destroy it on unmount. Call
map.remove(). A map that is not removed keeps its WebGL context, its workers and its event listeners, and browsers cap live contexts at around 8 to 16. Past the cap, existing maps go blank rather than throwing. - Do not put the map instance in reactive state. Frameworks deep-proxy or diff whatever you hand them. A map instance is a large object graph with circular references. Keep it in a plain ref.