Leaflet-Geoman with Next.js App Router
Leaflet needs browser APIs. In Next.js App Router, keep the page and its metadata server-rendered while loading the interactive map inside a small client component.
Create a Next.js application
npx create-next-app@latest geoman-next --ts --app --src-dir
cd geoman-next
npm install [email protected] @geoman-io/[email protected]
npm install --save-dev @types/leaflet
Use the src/ directory and App Router in the setup prompts. The paths below assume those choices.
Add the map component
Create src/components/GeomanMap.tsx with the component below. It is the same map component as the React guide, including cleanup for React StrictMode.
import React, { useEffect, useRef, useState } from "react"
import * as L from "leaflet"
import "@geoman-io/leaflet-geoman-free"
import "leaflet/dist/leaflet.css"
import "@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css"
export default function GeomanMap() {
const container = useRef<HTMLDivElement>(null)
const features = useRef<L.FeatureGroup | null>(null)
const [geojson, setGeojson] = useState("")
useEffect(() => {
if (!container.current) return
const map = L.map(container.current).setView([51.505, -0.09], 13)
// A token-free example basemap. Choose a production tile provider for your app.
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map)
const drawn = L.featureGroup().addTo(map)
features.current = drawn
map.pm.addControls({
drawMarker: false,
drawCircle: false,
drawCircleMarker: false,
})
const onCreate: L.PM.CreateEventHandler = (event) =>
drawn.addLayer(event.layer)
const onRemove: L.PM.RemoveEventHandler = (event) =>
drawn.removeLayer(event.layer)
map.on("pm:create", onCreate)
map.on("pm:remove", onRemove)
// Cleanup also makes React StrictMode's development remount safe.
return () => {
map.off("pm:create", onCreate)
map.off("pm:remove", onRemove)
map.remove()
features.current = null
}
}, [])
return (
<section>
<div ref={container} style={{ height: "60vh", minHeight: 320 }} />
<button
type="button"
onClick={() =>
setGeojson(
JSON.stringify(features.current?.toGeoJSON(), null, 2) ?? "",
)
}
>
Export GeoJSON
</button>
<pre aria-label="Exported GeoJSON">{geojson}</pre>
</section>
)
}
The styles come from the installed npm packages. No CDN script tags are needed: loading Leaflet or Geoman twice can create conflicting instances.
Isolate the browser-only import
Create src/components/MapClient.tsx:
"use client"
import dynamic from "next/dynamic"
const GeomanMap = dynamic(() => import("./GeomanMap"), {
ssr: false,
loading: () => <p style={{ minHeight: 320 }}>Loading the drawing map…</p>,
})
export default function MapClient() {
return <GeomanMap />
}
Put ssr: false in this client wrapper. Next.js does not allow that option in a Server Component. A 'use client' directive alone does not prevent a component from being prerendered, which is why this dynamic boundary matters for Leaflet imports.
Keep the page server-rendered
Replace src/app/page.tsx with:
import type { Metadata } from "next"
import MapClient from "../components/MapClient"
export const metadata: Metadata = {
title: "GeoJSON drawing example",
description: "Draw and edit geographic features with Leaflet-Geoman.",
}
export default function Page() {
return (
<main>
<h1>Draw and edit GeoJSON</h1>
<MapClient />
</main>
)
}
Keep the generated root layout.tsx. The page heading and metadata are available before JavaScript loads; the map initializes in the browser. Run npm run dev, draw a polygon and use Export GeoJSON. Also run npm run build before deploying to catch accidental server imports.
Troubleshooting
If you see window is not defined, check that no Server Component directly imports GeomanMap or Leaflet. Import the MapClient wrapper instead. If the map is blank, check its container height and tile requests. If remounting duplicates controls, retain the map cleanup in the effect.
Continue with Leaflet installation and APIs, the live demo, or Free and Pro pricing.
Want more geo-editing insights?
Get tips, tutorials, and product updates delivered to your inbox.
No spam. Unsubscribe anytime.
