Making React-Leaflet work with NextJS
Tags: react, nextjs, javascript, leaflet
Published: 2023-06-23 08:55:10

I've ran into some issues implementing React Leaflet with NextJS for our admin panel at [PlaceKit](https://placekit.io). 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`](https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#nextdynamic) to make it lazy load, making the SSR pass simply ignore it:


```jsx
// 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](https://react-leaflet.js.org), 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`:

```jsx
// 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:

```jsx
// 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 forward `ref` or are using front-end specific features, to be lazy-loaded by `MapComponents.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:


```jsx
// 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:

```jsx
// 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:

```jsx
// 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>`:

```jsx
// 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:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hs68qayvps74mk8q6p1d.png)



---

So we've seen how to:
1. lazy load components with `next/dynamic`,
2. make `ref` work with lazy-loaded components,
3. dynamically load `leaflet` to access its methods like `L.Icon`,
4. 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](https://placekit.io) :)!