Use Leaflet and React Leaflet in Next.js
Leaflet reads window as soon as its module loads, and Next.js renders every page on the server
first, where there is no window. The build then stops with:
Error occurred prerendering page "/map".
ReferenceError: window is not definedIt happens even when the map component starts with "use client", because client components are
still rendered on the server for the first HTML. The fix is to load the map only in the browser.
The map component
This is an ordinary React Leaflet component. Only the key’s environment variable is specific to Next.js.
components/LeafletMap.jsx:
"use client";
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import "./leaflet-icons";
const KEY = process.env.NEXT_PUBLIC_MAPTOOLKIT_KEY;
export default function LeafletMap() {
return (
<MapContainer center={[47.2692, 11.4041]} zoom={13} style={{ height: "400px" }}>
<TileLayer
url={`https://rtc-cdn.maptoolkit.net/rtc/maptoolkit-maptoolkit.summer/{z}/{x}/{y}{r}.png?api_key=${KEY}`}
maxZoom={18}
attribution='© <a href="https://www.maptoolkit.com">Maptoolkit</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
/>
<Marker position={[47.2692, 11.4041]}>
<Popup>Innsbruck</Popup>
</Marker>
</MapContainer>
);
}Load it in the browser only
next/dynamic with ssr: false keeps the map out of the server render. In the App Router that
call is only allowed in a Client Component. Placed in a page, which is a Server Component by
default, the build fails with:
`ssr: false` is not allowed with `next/dynamic` in Server Components. Please move it into a Client Component.So the dynamic import goes into a small client wrapper of its own.
components/Map.jsx:
"use client";
import dynamic from "next/dynamic";
const LeafletMap = dynamic(() => import("./LeafletMap"), {
ssr: false,
loading: () => <div style={{ height: 400 }} />,
});
export default function Map() {
return <LeafletMap />;
}app/map/page.jsx:
import Map from "@/components/Map";
export default function Page() {
return <Map />;
}The page stays a Server Component and can fetch data as usual, and the map loads once the page is
in the browser. Give the loading placeholder the same height as the map, so the page does not
jump when the map appears.
In the Pages Router the wrapper is not needed: call dynamic() with ssr: false directly in the
page file.
The marker icon
After a build, Marker shows a broken image: Leaflet looks for its icon files next to its
stylesheet, and the bundler has moved them. Import the images and pass their URLs to Leaflet.
components/leaflet-icons.js:
import L from "leaflet";
import icon from "leaflet/dist/images/marker-icon.png";
import iconRetina from "leaflet/dist/images/marker-icon-2x.png";
import shadow from "leaflet/dist/images/marker-shadow.png";
// Next.js with webpack imports images as objects with a `src`; Turbopack as a URL string.
const url = (image) => image.src ?? image;
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconUrl: url(icon),
iconRetinaUrl: url(iconRetina),
shadowUrl: url(shadow),
});The url() helper is the Next.js part. Turbopack, the default bundler in Next.js 16, hands
the import over as a URL string. With next build --webpack, the same import is an object with a
src field, and passing it to Leaflet unchanged makes it request /[object Object]. The helper
works with both.
The file imports Leaflet, so it has to be loaded from the client-only component, as
LeafletMap.jsx does, never from a page or layout.
The API key
NEXT_PUBLIC_ variables are written into the JavaScript the browser downloads, which a map key
needs, because the browser sends it with every tile request. A variable without the prefix is
undefined in the browser, so the tile URL goes out with api_key=undefined. Restrict the key to
your domains in your account.
Next steps
With the map loading, everything on React Leaflet applies
unchanged, including useMap() for moving the map from your own state. For data that belongs
to the page, fetch it in the Server Component and pass it down to the map as props; it arrives
in the browser as part of the page instead of as a second request.
Using Maptoolkit maps in Next.js covers the same setup for Maptoolkit Maps JS.