2024: Nextjs 14 + react-leaflet 4
Find the detailed explanations with examples below or see the solution in action with a live example: codesandbox.
Explanation
After the explanations two examples will follow, one for a client
and one for a server
component.
window is not defined
This error occurs because nextjs
renders every component by default on the server first. This is super useful for things like fetching data. But leaflet
needs a browser window to display its content and there is no browser window on the server. So if we don't tell nextjs
to not render the map component on the server, leaflet
will throw an error, telling us that it couldn't find a browser window.
It is not enough to flag the leaflet
component as a client component with use client
. We still run into errors. The reason behind this is that leaflet
will try to render before react
finishes loading. We can postpone loading leaflet
with next/dynamic
.
marker-icon.png 404 (Not Found)
This error occurs with bundlers like webpack
that modify URLs in CSS. This behavior clashes with leaflet
's own dynamic creation of image urls. Fortunately this is easily fixable by installing leaflet-defaulticon-compatibility
and doesn't require manual tweaks.
Using page.tsx
as a client component
Setup nextjs
and leaflet
, install leaflet-defaulticon-compatibility:
npx create-next-app@latest
npm install leaflet react-leaflet leaflet-defaulticon-compatibility
npm install -D @types/leaflet
Important steps:
- import js for
leaflet
- import css and js for
leaflet-defaulticon-compatibility
- set
width
and height
for the map container
components/Map.tsx
"use client";
// IMPORTANT: the order matters!
import "leaflet/dist/leaflet.css";
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.webpack.css";
import "leaflet-defaulticon-compatibility";
import { MapContainer, Marker, Popup, TileLayer } from "react-leaflet";
export default function Map() {
const position = [51.505, -0.09]
return (
<MapContainer
center={position}
zoom={11}
scrollWheelZoom={true}
{/* IMPORTANT: the map container needs a defined size, otherwise nothing will be visible */}
style={{ height: "400px", width: "600px" }}
>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={position}>
<Popup>
This Marker icon is displayed correctly with <i>leaflet-defaulticon-compatibility</i>.
</Popup>
</Marker>
</MapContainer>
);
}
Import the map component with next/dynamic
. This has to be done in a different file from the one where the map component lives and it has to be done in the top level of the file, not inside another component. Otherwise it won't work:
app/page.tsx
"use client";
import dynamic from "next/dynamic";
const LazyMap = dynamic(() => import("@/components/Map"), {
ssr: false,
loading: () => <p>Loading...</p>,
});
export default function Home() {
return (
<main>
<LazyMap />
</main>
);
}
Using page.tsx
as a server component
If you want your page to be a server component, you can move the client component down the tree and pass data as props.
components/Map.tsx
"use client";
import { useState } from 'react';
/* ... more imports ... */
export default function Map({ initialData }) {
const [data, setData] = useState(initialData);
/* ... more code ... */
}
components/MapCaller.tsx
'use client';
import dynamic from 'next/dynamic';
const LazyMap = dynamic(() => import("@/components/Map"), {
ssr: false,
loading: () => <p>Loading...</p>,
});
function MapCaller(props) {
return <LazyMap {...props} />;
}
export default MapCaller;
app/page.tsx
import MapCaller from '@/components/MapCaller';
/* your fetch function that fetches data server side */
import { fetchData } from '@/lib/data';
export default async function Page() {
const data = await fetchData();
return <MapCaller initialData={data} />;
}