Skip to content

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 defined

It 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='&copy; <a href="https://www.maptoolkit.com">Maptoolkit</a> &copy; <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.