How to use react-leaflet in Nextjs with TypeScript (Surviving it)

Andres Prieto
7 min readMar 1, 2024

--

Simple map using OpenStreetMap

Quick solution for stressed people

// src/page.tsx

import dynamic from "next/dynamic";
import { useMemo } from "react";

export default async function Page() {
const Map = useMemo(() => dynamic(
() => import('@/components/map/'),
{
loading: () => <p>A map is loading</p>,
ssr: false
}
), [])

return (
<>
<div className="bg-white-700 mx-auto my-5 w-[98%] h-[480px]">
<Map posix={[4.79029, -75.69003]} />
</div>
</>
)
}
// src/components/map/index.tsx

"use client"

import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import { LatLngExpression, LatLngTuple } from 'leaflet';

import "leaflet/dist/leaflet.css";
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
import "leaflet-defaulticon-compatibility";

interface MapProps {
posix: LatLngExpression | LatLngTuple,
zoom?: number,
}

const defaults = {
zoom: 19,
}

const Map = (Map: MapProps) => {
const { zoom = defaults.zoom, posix } = Map

return (
<MapContainer
center={posix}
zoom={zoom}
scrollWheelZoom={false}
style={{ height: "100%", width: "100%" }}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={posix} draggable={false}>
<Popup>Hey ! I study here</Popup>
</Marker>
</MapContainer>
)
}

export default Map

What are we doing?

We are basically using Leaflet to render a dynamic map using Nextjs v14 with TypeScript from a simple new basic app we can implement a map like this. What we need to make it works are the following dependecies

npm i @types/leaflet react-leaflet leaflet leaflet-defaulticon-compatibility leaflet-geosearch

Those dependencies are used by react-leaflet to handle with Leaflet, its compatiblity with Nextjs and TypeScript types. Once the our dependecies are installed we can use react-leaflet module with its components to create a some maps, so we can create a simple component like map.tsxor what I recommend a directory called map/index.tsxthat directory can be useful if we can increase the complexity of our map, see this example from Richard Unterberg.

Step by step explanation

First we need a provider, I used OpenStreetMap to support (announce) open source projects, but there are some others like Mapbox, you can check on Leaflet documentation. With our provider we need some coordinates for example map=15/4.6945/-74.1410 corresponds with “Aeropuesto El dorado, Colombia” if we split the map coordinates we get the following varibales zoom=15 x=4.6945 y=-74.1410 those are the property we can find at Open Street Map url each time we open its website or when we click something in a map, also the ones we will use them for set the position we want to show in our component

Setting the map

First we should fix our TS types, as we are going to receive them from a props, also have to handle with some matters, so I rather an interface

interface MapProps {
xposix: number,
yposix: number,
zoom?: number,
}

Doing it this way TS will send a warning about types, for xposix and yposix so we would change that implementation, as we will need to make an array we could make it like this

interface MapProps {
posix: number[],
zoom?: number,
}

But, this way React-Leaflet will throw an error, because types won’t match, to avoid that we can use our @types/leaflet module as follows

import { LatLngExpression, LatLngTuple } from 'leaflet';
// ...
interface MapProps {
posix: LatLngExpression | LatLngTuple,
zoom?: number,
}

This way we will match types for properties center from <MapContainer> and position from <Marker>

Once our types match each other we can move into another matter, the <MapContainer> to create a container we simple need to import it then give it a center which the coordinates in the map it will render, also it’s common to give it a zoom to use an especific scale instead of the default one, at this point it’s important to import the leaflet styles

import { MapContainer } from "react-leaflet";
import { LatLngExpression, LatLngTuple } from 'leaflet';

import "leaflet/dist/leaflet.css";
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
import "leaflet-defaulticon-compatibility";

interface MapProps {
posix: LatLngExpression | LatLngTuple,
zoom?: number,
}

const Map = (Map: MapProps) => {
return (
<MapContainer
zoom={Map.zoom}
center={Map.posix}
scrollWheelZoom={false}
style={{ height: "100%", width: "100%" }}
>
</MapContainer>
)
}

At this point we will have a simple blank box without a map, that’s because there’s no any provider yet

Attaching a provider

To add a provider we need to use another component inside our <MapContainer> that component usually is <TitleLayer> which will be the name of our layer and will work as provider, we are using Open Street Map for this example

<MapContainer
center={Map.posix}
zoom={Map.zoom}
scrollWheelZoom={false}
style={{ height: "100%", width: "100%" }}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={Map.posix} draggable={false} />
</MapContainer>

I also added a Marker to using the same posix of center that works to show points in a map, yet we can use some other interesting tools like popups or tooltips, we just need to place them inside the marker component

Adding some tools

Let’s make a simple example, we can use the <Popup> component to create a simple popup when we click on the marker

<MapContainer
center={Map.posix}
zoom={Map.zoom}
scrollWheelZoom={false}
style={{ height: "100%", width: "100%" }}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={Map.posix} draggable={false}>
<Popup>Hey ! I study here</Popup>
</Marker>
</MapContainer>

Another useful tool from JS is destructuring, we can destructure on TS too, so we can make our lifes easier if we split our props

const Map = (Map: MapProps) => {
const { zoom, posix } = Map
// ...
}

export default Map

We can also set default values for our components to meet the interface requirements easier as follows

const defaults = { zoom: 19 }

const Map = (Map: MapProps) => {
const { zoom = defaults.zoom, posix } = Map
// ...
}

export default Map

Our final component should look like this

"use client"

import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import { LatLngExpression, LatLngTuple } from 'leaflet';

import "leaflet/dist/leaflet.css";
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
import "leaflet-defaulticon-compatibility";

interface MapProps {
posix: LatLngExpression | LatLngTuple,
zoom?: number,
}

const defaults = {
zoom: 19,
}

const Map = (Map: MapProps) => {
const { zoom = defaults.zoom, posix } = Map

return (
<MapContainer
center={posix}
zoom={zoom}
scrollWheelZoom={false}
style={{ height: "100%", width: "100%" }}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={posix} draggable={false}>
<Popup>Hey ! I study here</Popup>
</Marker>
</MapContainer>
)
}

export default Map

WARNING!!

There are 2 extremely important issue to mention, the first one is that React-Leaflet doesn’t support SSR due to Leaflet construction, according to its documentation there’re limitations that’s why we use the directrice "use client" in out map component

Leaflet makes direct calls to the DOM when it is loaded, therefore React Leaflet is not compatible with server-side rendering.

The second important issue we will face is about dynamic as we will use it we must use a default export so using a component like this

export const Map = (Map: MapProps) => {}

Won’t work at all, you need to use the export default <COMPONENT> instruction to make it work with dynamic

const Map = (Map: MapProps) => {}
export default Map

Moving components to Nextjs

In an ideal world we could be able to simple import our CSR component <Map /> into our SSR page http://localhost:3000/maps

import Map from "@/components/map";

export default function Page() {
return (
<>
<div className="bg-white-700 mx-auto my-5 w-[98%] h-[480px]">
<Map posix={[4.79029, -75.69003]} />
</div>
</>
)
}

But, that’s a fairy dream if you do that it won’t work at all, we need to use a different strategy to achieve a couple of key features, which are basically 3:

  • Importing the component (dynamic)
  • Control component render(useMemo)
  • Handle with SSR and CSR (dynamic)

To solve those issues we can handle them one by one

Importing the component

As a SSR component our page can’t handle with map importing by default that would produce an error, so we can use dynamic from Next to import a component “turning off” the SSR strategy, but this will import it directly once…

import dynamic from "next/dynamic";

export default async function Page() {
const Map = dynamic(() => import('@/components/map/'), { ssr: false })

return (
<>
<div className="bg-white-700 mx-auto my-5 w-[98%] h-[480px]">
<Map posix={[4.79029, -75.69003]} />
</div>
</>
)
}

Control component render

A way to solve that issue of importing directly our map can be to use a hook, for example useMemo this hook will storage in cache our map

import dynamic from "next/dynamic";
import { useMemo } from "react";

export default async function Page() {
const Map = useMemo(() => dynamic(
() => import('@/components/map/'),
{ ssr: false }
), [])

return (
<>
<div className="bg-white-700 mx-auto my-5 w-[98%] h-[480px]">
<Map posix={[4.79029, -75.69003]} />
</div>
</>
)
}

Handle with SSR and CSR

CSR strategy needs time to render its content, so to make your component perform a better UX, we can simple use the loading option to show something while the component is being rendered

import dynamic from "next/dynamic";
import { useMemo } from "react";

export default async function Page() {
const Map = useMemo(() => dynamic(
() => import('@/components/map/'),
{
loading: () => <p>A map is loading</p>,
ssr: false
}
), [])

return (
<>
<div className="bg-white-700 mx-auto my-5 w-[98%] h-[480px]">
<Map posix={[4.79029, -75.69003]} />
</div>
</>
)
}

Some useful references

--

--

Andres Prieto

I'm a web developer & std. of system and computational engineering, who teach himself as much as possible, I GNU/Linux since I met it and I loading what I learn