PaulLeCam/react-leaflet

Support server-side rendering

dmitry opened this issue ยท 17 comments

https://github.com/PaulLeCam/react-leaflet#technical-considerations have the following point:

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

I would like to discuss of the possibilities how it's possible to add server-side support for this library.

My current solution just skip rendering the leaflet on server-side, and render just on client-side:

  render() {
    const position = [51.505, -0.09];

    if (process.env.BROWSER) {
      var {Map, Marker, Popup, TileLayer} = require('react-leaflet');
      return (
        <div className="search-map">
          <Map center={position} zoom={13}>
            <TileLayer
              url='http://{s}.tile.osm.org/{z}/{x}/{y}.png'
              attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
              />
            <Marker position={position}>
              <Popup>
                <span>A pretty CSS3 popup.<br/>Easily customizable.</span>
              </Popup>
            </Marker>
          </Map>
        </div>
      );
    }
    else {
      return null;
    }
  }

I don't believe it's a best practice, but still it's something, than nothing, and I think it's better than using the headless-leaflet fork.

The issue is with Leaflet itself, as it doesn't check if the DOM is available when it's loaded, not directly this library.
I do not plan to support server-side rendering in this lib until it is supported by Leaflet itself, unless it's a trivial implementation with no side-effect.

The solution you presented may work for you, but it's completely dependant on your environment and your build, it would probably not work in other cases.

iam4x commented

@PaulLeCam We could provide a React Component as fallback for server side rendering, check if window exists render the map or render the fallback? Something like that:

<Map .... fallback={ReactComponent}>
  ...
</Map>

@iam4x Feel free to implement any workaround in your app... As I previously explained, this issue is not caused by this lib.

Thanks for the workarounds gents, I ended up just using a client side bower package and not the react package, but will switch back now I know the render could be defined conditionally based on the environment

@dougajmcdonald similar idea I'm using too. It's more flexible and maintainable.

@dougajmcdonald I was wondering what was the solution for this? You guys are loading leaflet in with bower, and then proceeding to require react-leaflet the same way? Or are you guys also using bower to load react-leaflet?

May someone give the example of this workaround

It's ugly, but it works.

let Map, MapComponents
class LeafletMap extends Component {

  componentDidMount(){
    //Only runs on Client, not on server render
    Map = require('react-leaflet').Map
    MapComponents = require('./mapComponents').default
    this.forceUpdate()
  }

  render () {
    return (
      (Map)
      ? (
        <Map
          zoom={10}
          maxZoom={18}
          minZoom={9}
        >
          <MapComponent />
        </Map>
      )
      : (null)
      }
    )
  }
}
rktel commented

it works, too.

In index.html:

Include ..rel="stylesheet" href="https://unpkg.com/leaflet@1.2.0/dist/leaflet.css"..

In React Component

`import React, { Component } from 'react'

class Mapa extends Component {

componentWillMount() {
    console.log('componentWillMount')
    Map = require('react-leaflet').Map
    TileLayer = require('react-leaflet').TileLayer
    TileLayer = require('react-leaflet').TileLayer
    Marker = require('react-leaflet').Marker
    Popup = require('react-leaflet').Popup
}

render() {
    const position = [-12.76767, -76.343434]
    return (

            <Map center={position} zoom={10} style={{ height: "100vh" }}>
                <TileLayer
                    attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
                    url='http://{s}.tile.osm.org/{z}/{x}/{y}.png'
                />
                <Marker position={position}>
                    <Popup>
                        <span>A pretty CSS3 popup. <br /> Easily customizable.</span>
                    </Popup>
                </Marker>
            </Map>


    )
}

}

export default Mapa`

rktel how to extends MapControl with componentWillMount().
I want add search box by https://github.com/smeijer/leaflet-geosearch.

For anyone wondering I managed to get this working using react-loadable for now

In your parent component do something like so:
const LoadableSearchResultMap = Loadable({ loader: () => import('./SearchResultMap') as Promise<any>, loading() { return <div>Loading...</div> } });

Then I made a component called SearchResultMap.tsx and inside is:

`import * as React from 'react';
import { Map, Marker, Popup, TileLayer, LayersControl } from 'react-leaflet';
import ReactLeafletGoogleLayer from 'react-leaflet-google-layer';
import { LatLngExpression } from 'leaflet';

export default class SearchResultMap extends React.Component {
mapSettings = {
zoom: 15,
center: [48.067539, 12.862530] as LatLngExpression
}

render() {
    return <><Map {...this.mapSettings}><ReactLeafletGoogleLayer googleMapsLoaderConf={{ KEY: 'Key Goes Here' }} type={'terrain'} /></Map></>
}

}`

The cleanest solution i found is here : https://github.com/etalab/adresse.data.gouv.fr/blob/0bc6dd7/pages/map.js#L5-L12

Sorry, but how i pass props with this dynamic

Kaz-z commented

Hopefully this will help someone...If you're using gatsby then for some reason using the react-leaflet gatsby plugin works rather than using the library straight...weird but it saved me a lot of headache.

This might be helpful for those who use Webpack & es6 dynamic import feature:

import React, { PureComponent } from 'react';

class MapLoader extends PureComponent {
  constructor(props) {
    super(props);

    this.state = { loading: true };
    this.Map= null;
  }

  componentDidMount() {
    import(/* webpackMode: "eager" */ './Map').then((module) => {
      this.Map = module.default;
      this.setState({ loading: false });
    });
  }

  render() {
    const { Map, props, state: { loading } } = this;

    if (loading) {
       return null;// or render a loading
    }

    return <Map {...props} />;
  }
}

export default MapLoader;

This worked for me in React 18. MyMap is the component that has the leaflet imports. Then you can use Suspense if you want to render something else while it is importing.

const MyMap = React.lazy(() => import('./MyMap'));