I've ran into some issues implementing React Leaflet with NextJS for our admin panel at PlaceKit. So let's gather my findings into a single article, hoping it'll save you some time.
Because NextJS has an SSR (server-side rendering) layer, importing third-party front-end libraries sometimes results in headaches.
Most of the time, you just have to wrap your front-end component with next/dynamic
to make it lazy load, making the SSR pass simply ignore it:
// MyComponent.jsx
import frontLib from '<your-front-end-library>';
const MyComponent = (props) => {
// do something with frontLib
};
export default MyLazyComponent;
// MyPage.jsx
import dynamic from 'next/dynamic';
const MyComponent = dynamic(
() => import('./MyComponent'),
{
ssr: false,
loading: () => (<div>loading...</div>),
}
);
const MyPage = (props) => (
<MyComponent />
);
But in the case of React Leaflet, you may need to put in some more efforts.
Passing ref
If you simply assign a ref
to lazy-loaded <MapContainer>
, you'll get a unusable proxy reference coming from dynamic
:
// Map.jsx
import dynamic from 'next/dynamic';
import { useEffect, useRef } from 'react';
const MapContainer = dynamic(
() => import('react-leaflet').then((m) => m.MapContainer),
{ ssr: false }
);
const Map = (props) => {
const mapRef = useRef(null);
useEffect(
() => console.log(mapRef.current), // { retry: fn, ... }
[mapRef.current]
);
return (
<MapContainer ref={mapRef} ?>
);
};
export default Map;
The trick is a bit bulky, but you have to wrap it under another component and forward the ref
as a standard property (mapRef
here), and you lazy load that one instead:
// MapLazyComponents.jsx
import {
MapContainer as LMapContainer,
} from 'react-leaflet';
export const MapContainer = ({ forwardedRef, ...props }) => (
<LMapContainer {...props} ref={forwardedRef} />
);
// Map.jsx
import dynamic from 'next/dynamic';
import { forwardRef, useEffect, useRef } from 'react';
const LazyMapContainer = dynamic(
() => import('./MapLazyComponents').then((m) => m.MapContainer),
{ ssr: false }
);
const MapContainer = forwardRef((props, ref) => (
<LazyMapContainer {...props} forwardedRef={ref} />
));
const Map = (props) => {
const mapRef = useRef(null);
useEffect(
() => console.log(mapRef.current), // this works!
[mapRef.current]
);
return (
<MapContainer ref={mapRef} />
);
};
export default Map;
Organizing your components
As we'll be preparing a few other React Leaflet components in the following examples, let's reorganise this into 3 files:
-
Map.jsx
: your final component or page showing the map. -
MapComponents.jsx
: components that will lazy-load the React Leaflet ones. These will be ready to import as-is. -
MapLazyComponents.jsx
: wrappers that forwardref
or are using front-end specific features, to be lazy-loaded byMapComponents.jsx
.
Let's also add <TileLayer>
and <ZoomControl>
as we won't need any specific changes apart from loading them with dynamic
.
So at this point you get:
// MapLazyComponents.jsx
import {
MapContainer as LMapContainer,
} from 'react-leaflet';
export const MapContainer = ({ forwardedRef, ...props }) => (
<LMapContainer {...props} ref={forwardedRef} />
);
// MapComponents.jsx
import dynamic from 'next/dynamic';
import { forwardRef } from 'react';
export const LazyMapContainer = dynamic(
() => import('./MapLazyComponents').then((m) => m.MapContainer),
{
ssr: false,
loading: () => (<div style={{ height: '400px' }} />),
}
);
export const MapContainer = forwardRef((props, ref) => (
<LazyMapContainer {...props} forwardedRef={ref} />
));
// direct import from 'react-leaflet'
export const TileLayer = dynamic(
() => import('react-leaflet').then((m) => m.TileLayer),
{ ssr: false }
);
export const ZoomControl = dynamic(
() => import('react-leaflet').then((m) => m.ZoomControl),
{ ssr: false }
);
// Map.jsx
import { useEffect, useRef } from 'react';
// import and use components as usual
import { MapContainer, TileLayer, ZoomControl } from './MapComponents.jsx';
const Map = (props) => {
const mapRef = useRef(null);
return (
<MapContainer
ref={mapRef}
touchZoom={false}
zoomControl={false}
style={{ height: '400px', zIndex: '0!important' }}
>
<TileLayer url="..." attribution="..." style={{ zIndex: '0!important' }} />
<ZoomControl position="topright" style={{ zIndex: '10!important' }} />
</MapContainer>
);
};
export default Map;
Using custom Marker icons
Alright, now that we start having a Map, let's add a marker. But most of the time you'd want to use a custom icon with it.
Custom Marker icons need to use L.Icon()
from leaflet
itself, which is a library instantiating stuff in window
, so it breaks SSR when importing in Next. But it can not be loaded with dynamic()
or even with React.lazy()
which are exclusive to lazy loading components.
So, let's wrap our <Marker>
component in MapLazyComponents.jsx
as it'll be depending on front-end exclusive features:
// MapLazyComponents.jsx
import { useEffect, useState } from 'react';
import {
MapContainer as LMapContainer,
Marker as LMarker,
} from 'react-leaflet';
// ...
export const Marker = ({ forwardedRef, icon: iconProps, ...props }) => {
const [icon, setIcon] = useState();
useEffect(
() => {
// loading 'leaflet' dynamically when the component mounts
const loadIcon = async () => {
const L = await import('leaflet');
setIcon(L.icon(iconProps));
}
loadIcon();
},
[iconProps]
);
// waiting for icon to be loaded before rendering
return (!!iconProps && !icon) ? null : (
<LMarker
{...props}
icon={icon}
ref={forwardedRef}
/>
);
};
// MapComponents.jsx
// ...
const LazyMarker = dynamic(() => import('./MapLazyComponents').then((m) => m.Marker), { ssr: false });
export const Marker = forwardRef((props, ref) => (
<LazyMarker {...props} forwardedRef={ref} />
));
// Map.jsx
// ...
import { MapContainer, TileLayer, ZoomControl, Marker } from './MapComponents.jsx';
import CustomIcon from '../public/custom-icon.svg';
const Map = (props) => {
const mapRef = useRef(null);
const markerRef = useRef(null);
return (
<MapContainer
ref={mapRef}
touchZoom={false}
zoomControl={false}
style={{ height: '400px', zIndex: '0!important' }}
>
<TileLayer url="..." attribution="..." style={{ zIndex: '0!important' }} />
<ZoomControl position="topright" style={{ zIndex: '10!important' }} />
<Marker
ref={markerRef}
icon={{
iconUrl: CustomIcon.src,
iconAnchor: [16,32],
iconSize: [32,32]
}}
style={{ zIndex: '1!important' }}
/>
</MapContainer>
);
};
//...
Handling map events
For marker events, you can already pass the eventHandlers
property and it'll work. But to handle map events, it can not be done on the <MapContainer>
component, you need to use the useMapEvents()
hook from React Leaflet in a child component.
Same here, we'll need to wrap it, and we'll do it within a custom <MapConsumer>
element to simplify things:
// MapLazyComponents.jsx
//...
import { useMapEvents } from 'react-leaflet/hooks';
export const MapConsumer = ({ eventsHandler }) => {
useMapEvents(eventsHandler);
return null;
};
// MapComponents.jsx
//...
export const MapConsumer = dynamic(
() => import('./MapLazyComponents').then((m) => m.MapConsumer),
{ ssr: false }
);
So in you Map.jsx
file, you're now able to add <MapConsumer>
in <MapContainer>
:
// Map.jsx
//...
const Map = (props) => {
const mapRef = useRef(null);
const markerRef = useRef(null);
const mapHandlers = useMemo(
() => ({
click(e) {
// center view on the coordinates of the click
// `this` is the Leaflet map object
this.setView([e.latlng.lat, e.latlng.lng]);
},
}),
[]
);
return (
<MapContainer
ref={mapRef}
touchZoom={false}
zoomControl={false}
style={{ height: '400px', zIndex: '0!important' }}
>
<TileLayer url="..." attribution="..." style={{ zIndex: '0!important' }} />
<ZoomControl position="topright" style={{ zIndex: '10!important' }} />
<MapConsumer
eventsHandler={mapHandlers}
/>
<Marker
ref={markerRef}
icon={{
iconUrl: CustomIcon.src,
iconAnchor: [16,32],
iconSize: [32,32]
}}
style={{ zIndex: '1!important' }}
/>
</MapContainer>
);
};
A few states and CSS later, here's my result:
So we've seen how to:
- lazy load components with
next/dynamic
, - make
ref
work with lazy-loaded components, - dynamically load
leaflet
to access its methods likeL.Icon
, - wrap
react-leaflet
custom hooks to handle events.
Adapting these tricks should cover most of your edge cases. I hope breaking down into these specific use-cases will help you work better with React Leaflet on NextJS!
And of course, if you need a reverse geocoding API to get coordinates from an address, have a look at PlaceKit.io :)!
Top comments (3)
Did you find this error:
11:29 Error: Component definition is missing display name react/display-name
Seems to be related to the usage of forwardRef, e.g.:
export const MapContainer = forwardRef((props, ref) => (
Suggested solution seems to define the object and separately export it (stackoverflow.com/a/69302038) but since there are several such objects in the file, I wonder if there's an easier way to do it?
Can this also increase the speed of react-leaflet in displaying thousands of markers without cluster-markers? in my case I need to display a lot of markers to mark each travel history point so I don't need cluster-markers but for some reason the Map becomes very slow and renders every time I drag/zoom
Hi, that's 100% related to Leaflet, and am not sure if there's a solution for your use-case. There must be some optimization to display more markers, but you'll always hit a performance limit.