Leaflet with next.js?
Asked Answered
L

9

35

I am getting a ReferenceError:

window is not defined when using next.js with leaflet.js .

Wondering if there's a simple solution to this problem - is using next.js overcomplicating my workflow?

for those curious with the exact code,

import React, { createRef, Component } from "react";
import L from "leaflet";
import { Map, TileLayer, Marker, Popup, DivOverlay } from "react-leaflet";
import axios from "axios";
import Header from "./Header";


export default class PDXMap extends Component {
  state = {
    hasLocation: false,
    latlng: {
      lat: 45.5127,
      lng: -122.679565
    },
    geoJSON: null
  };

  mapRef = createRef();

  componentDidMount() {
    this.addLegend();
    if (!this.state.hasLocation) {
      this.mapRef.current.leafletElement.locate({
        setView: true
      });
    }
    axios
      .get(
        "https://opendata.arcgis.com/datasets/40151125cedd49f09d211b48bb33f081_183.geojson"
      )
      .then(data => {
        const geoJSONData = data.data;
        this.setState({ geoJSON: geoJSONData });
        return L.geoJSON(this.state.geoJSON).addTo(
          this.mapRef.current.leafletElement
        );
      });
  }

  handleClick = () => {
    this.mapRef.current.leafletElement.locate();
  };

  handleLocationFound = e => {
    console.log(e);
    this.setState({
      hasLocation: true,
      latlng: e.latlng
    });
  };

  getGeoJsonStyle = (feature, layer) => {
    return {
      color: "#006400",
      weight: 10,
      opacity: 0.5
    };
  };

  addLegend = () => {
    const map = this.mapRef.current.leafletElement;
    L.Control.Watermark = L.Control.extend({
      onAdd: function(map) {
        var img = L.DomUtil.create("img");

        img.src = "https://leafletjs.com/docs/images/logo.png";
        img.style.width = "200px";

        return img;
      }
    });

    L.control.watermark = function(opts) {
      return new L.Control.Watermark(opts);
    };

    L.control.watermark({ position: "bottomleft" }).addTo(map);
  };

  render() {
    const marker = this.state.hasLocation ? (
      <Marker position={this.state.latlng}>
        <Popup>
          <span>You are here</span>
        </Popup>
      </Marker>
    ) : null;

    return (
      <Map
        className="map-element"
        center={this.state.latlng}
        length={4}
        onClick={this.handleClick}
        setView={true}
        onLocationfound={this.handleLocationFound}
        ref={this.mapRef}
        zoom={14}
      >
        <TileLayer
          attribution='&amp;copy <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
        {marker}
      </Map>
    );
  }
}

/**
 * TODO:  Add Header + Legend to map
 *        - Header to be styled
 *        - Legend to be present in header
 *
 */


import React from 'react';
import PDXMap from "../components/map";


export default function SignIn() {
  const classes = useStyles();

  return (
      <PDXMap/>
);
}

I'm happy to use any way forward - just interested in getting a functional product.

Cheers!

Update

Hey everyone,

I am still getting this error (came back to this a bit later than I had planned haha).

I am currently using this approach with useEffects,

import React, {useEffect, useState} from 'react';

function RenderCompleted() {

    const [mounted, setMounted] = useState(false);

    useEffect(() => {
        setMounted(true)

        return () => {
            setMounted(false)
        }
    });

    return mounted;
}

export default RenderCompleted;

and this is the page it is showing on

import React, { useEffect } from "react";
import Router, { useRouter } from "next/router";
import { useRef, useState } from "react";


//viz
import PDXMap from "../../components/Visualization/GIS/map";

import RenderCompleted from "../../components/utils/utils";

// import fetch from 'isomorphic-unfetch';
import { Cookies, CookiesProvider } from "react-cookie";
const cookies = new Cookies();
//containers

// Layouts
import Layout from "../../components/Layout/Layout_example";
import Chart from "../../components/Visualization/Graphs/Chart";
import Table from "../../components/Visualization/Tables/Table";
import Sidebar from "../../components/Layout/Sidebar/SidebarProperty";



export default function Bargains() {

  // const [inbrowser, setBrowser] = useState(false);

  const choiceRef = useRef<any>();
  const [message, setMessage] = useState<any>(null);

  const [productList, setProductList] = useState<any>([]);
  const [searched, setSearched] = useState(false);

  const router = useRouter();

  let token = cookies.get("token");

  // useEffect(() => {
  //   setBrowser(true);
  // });
  const isMounted = RenderCompleted();


  const columns = React.useMemo(
    () => [
    ....
    ],

    []
  )



  async function handleChoice() {

    console.log("searching...", choiceRef.current?.value);
    setMessage("Searching...");
    var headers = {
      "Content-Type": "application/x-www-form-urlencoded",
      "auth-token": token,
    };

    fetch(
    ....
  }


            <div className="flex flex-wrap ">
            {isMounted && <PDXMap/>}


              
              <Table columns={columns as any} data={productList as any} />


            </div>
          </div>



        </div>
      </div>


    </Layout>



  )
}

With the same error message of

ReferenceError: window is not defined

##update two

Okay, so oddly, it does work when I browse into the site from another page, but not when i load the page itself.

Will have a think on this, but perhaps it is because the map is loading data with componentDidMount() and that is interacting weirdly?

Update

Okay I've created a more simple example based on https://github.com/rajeshdh/react-leaflet-with-nextjs

Now it is loading, but the tiles are showing incorrectly, with some tiles not loading.

This is the map component I am using to be simple,

import React, { Component, createRef } from 'react';
import { Map, TileLayer, Marker, Popup, MapControl, withLeaflet } from 'react-leaflet';
import { GeoSearchControl, OpenStreetMapProvider } from 'leaflet-geosearch';


class SearchBox extends MapControl {
  constructor(props) {
    super(props);
    props.leaflet.map.on('geosearch/showlocation', (e) => props.updateMarker(e));
  }

  createLeafletElement() {
    const searchEl = GeoSearchControl({
      provider: new OpenStreetMapProvider(),
      style: 'bar',
      showMarker: true,
      showPopup: false,
      autoClose: true,
      retainZoomLevel: false,
      animateZoom: true,
      keepResult: false,
      searchLabel: 'search'
    });
    return searchEl;
  }
}


export default class MyMap extends Component {
  state = {
    center: {
      lat: 31.698956,
      lng: 76.732407,
    },
    marker: {
      lat: 31.698956,
      lng: 76.732407,
    },
    zoom: 13,
    draggable: true,
  }

  refmarker = createRef(this.state.marker)

  toggleDraggable = () => {
    this.setState({ draggable: !this.state.draggable });
  }

  updateMarker = (e) => {
    // const marker = e.marker;
    this.setState({
      marker: e.marker.getLatLng(),
    });
    console.log(e.marker.getLatLng());
  }

  updatePosition = () => {
    const marker = this.refmarker.current;
    if (marker != null) {
      this.setState({
        marker: marker.leafletElement.getLatLng(),
      });
    }
    console.log(marker.leafletElement.getLatLng());
  }

  render() {
    const position = [this.state.center.lat, this.state.center.lng];
    const markerPosition = [this.state.marker.lat, this.state.marker.lng];
    const SearchBar = withLeaflet(SearchBox);

    return (
      <div className="map-root">
        <Map center={position} zoom={this.state.zoom} style={{
                        height:"700px"
                    }}>
          <TileLayer
            attribution='&amp;copy <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
            url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
          />
          <Marker
            draggable={true}
            onDragend={this.updatePosition}
            position={markerPosition}
            animate={true}
            ref={this.refmarker}>
            <Popup minWidth={90}>
              <span onClick={this.toggleDraggable}>
                {this.state.draggable ? 'DRAG MARKER' : 'MARKER FIXED'}
              </span>
            </Popup>
          </Marker>
          <SearchBar updateMarker={this.updateMarker} />
        </Map>
        <style jsx>{`
                .map-root {
                  height: 100%;
                }
                .leaflet-container {
                 height: 400px !important;
                 width: 80%;
                 margin: 0 auto;
               }
           `}
        </style>
      </div>
    );
  }
}

And to call it, I am using this:

const SimpleExample = dynamic(() => import("../../components/Visualization/GIS/map"), {
  ssr: false
}); 

And have tried this:

{isMounted && <SimpleExample/>}
Lanford answered 29/8, 2019 at 6:27 Comment(4)
where do you see this error?Cornetcy
@Cornetcy I see this error in the browser when i go to the page aboveLanford
It doesn't make sense since window is available in the browser :] Can you make a small repo which reproduces this?Cornetcy
Yes I know right! Let me try to debug this issue more today :PLanford
C
10

window is not available in SSR, you probably get this error on your SSR env.

One way to solve this is to mark when the component is loaded in the browser (by using componentDidMount method), and only then render your window required component.

class MyComp extends React.Component {
  state = {
    inBrowser: false,
  };

  componentDidMount() {
    this.setState({ inBrowser: true });
  }

  render() {
    if (!this.state.inBrowser) {
      return null;
    }

    return <YourRegularComponent />;
  }
}

This will work cause componentDidMount lifecycle method is called only in the browser.

Edit - adding the "hook" way

import { useEffect, useState } from 'react';

const MyComp = () => {
  const [isBrowser, setIsBrowser] = useState(false);
  useEffect(() => {
    setIsBrowser(true);
  }, []);

  if (!isBrowser) {
    return null;
  }

  return <YourRegularComponent />;
};

useEffect hook is an alternative for componentDidMount which runs only inside the browser.

Cornetcy answered 29/8, 2019 at 7:43 Comment(0)
A
87

Answer for 2020

I also had this problem and solved it in my own project, so I thought I would share what I did.

NextJS can dynamically load libraries and restrict that event so it doesn't happen during the server side render. See the documentation for more details.

In my examples below I will use and modify example code from the documentation websites of both NextJS 10.0 and React-Leaflet 3.0.

Side note: if you use TypeScript, make sure you install @types/leaflet because otherwise you'll get compile errors on the center and attribution attributes.

To start, I split my react-leaflet code out into a separate component file like this:

import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'
import 'leaflet/dist/leaflet.css'

const Map = () => {
  return (
    <MapContainer center={[51.505, -0.09]} zoom={13} scrollWheelZoom={false} style={{height: 400, width: "100%"}}>
      <TileLayer
        attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      <Marker position={[51.505, -0.09]}>
        <Popup>
          A pretty CSS3 popup. <br /> Easily customizable.
        </Popup>
      </Marker>
    </MapContainer>
  )
}

export default Map

I called that file map.tsx and placed it in a folder called components but you might call it map.jsx if you don't use TypeScript.

Note: It is important that this code is in a separate file from where it is embedded into your page because otherwise you'll still get window undefined errors.

Also Note: don't forget to specify the style of the MapContainer component so it doesn't render as zero pixels in height/width. In the above example, I added the attribute style={{height: 400, width: "100%"}} for this purpose.

Now to use that component, take advantage of NextJS's dynamic loading like this:

import dynamic from 'next/dynamic'

function HomePage() {
  const Map = dynamic(
    () => import('@components/map'), // replace '@components/map' with your component's location
    { ssr: false } // This line is important. It's what prevents server-side render
  )
  return <Map />
}

export default HomePage

If you want the map to be replaced with something else while it's loading (probably a good practice) you should use the loading property of the dynamic function like this:

import dynamic from 'next/dynamic'

function HomePage() {
  const Map = dynamic(
    () => import('@components/map'), // replace '@components/map' with your component's location
    { 
      loading: () => <p>A map is loading</p>,
      ssr: false // This line is important. It's what prevents server-side render
    }
  )
  return <Map />
}

export default HomePage

Adrian Ciura commented on the flickering which may occur as your components re-render even when nothing about the map should change. They suggest using the new React.useMemo hook to solve that problem. If you do, your code might look something like this:

import React from 'react'
import dynamic from 'next/dynamic'

function HomePage() {
  const Map = React.useMemo(() => dynamic(
    () => import('@components/map'), // replace '@components/map' with your component's location
    { 
      loading: () => <p>A map is loading</p>,
      ssr: false // This line is important. It's what prevents server-side render
    }
  ), [/* list variables which should trigger a re-render here */])
  return <Map />
}

export default HomePage

I hope this helps. It would be easier if react-leaflet had a test for the existence of window so it could fail gracefully, but this workaround should work until then.

Ariose answered 1/11, 2020 at 17:23 Comment(17)
A big Thanks to you :) I was searching for this leaflet with nextjs for so long nothing work the way I want them to, as I was about to give up I stumbled upon your answer and it worked like magic in the first go. And to let you know the imports for Marker & Popup components are missing in the first snippet you'll want to add them.Rori
@Tusharsaxena Glad to hear it! Thanks for catching those missing imports too... I've added them now.Ariose
Thanks for this great answer. Works for me on the latest React17 and NextJs 10. One addition is to enclose the map in Memo, otherwise it will flicker on each render. i.e.: const Map = React.useMemo(() => dynamic(() => import('../components/Map'), { loading: () => <p>Loading map...</p>, ssr: false, }), []) Ewall
@AdrianCiura Brilliant suggestion! I'll incorporate it in my answer.Ariose
how do you do if you want to pass values to @components/map? like in my case my map recieves a positions that is used to click the map and save longitude and latitude, something like this: const Map =({selectedPosition,setSelectedPosition}) =>{... }Mycenaean
Thank you for your answer. Any workaround on IE for this issue since IE doesn't support import()?Sallyanne
@Sallyanne I don't know but that's a good question in it's own right.Ariose
Hi, Thank you for your answer. Although, its useless to import inside the calling component, it should be moved outside, as follows: const Map = dynamic(() => import('path/to/Map.tsx'), { // eslint-disable-next-line react/display-name loading: () => <Loader />, ssr: false, }); \n\n const Home = () => <Map />;Mayes
@AdrianCiura You just saved me from throwing my laptop out the window. Thank you!Colenecoleopteran
This solved my issue with NextJS 12, react-leaflet 3Invigilate
i have still the problem that if a form is on the same page the map keeps flickering very annoying cant figure out why, will remove the leaflet map for now .... accept that everything else is working ok.Fezzan
@mangrove108 are you sure your re-render variables aren't changing? I think it's considered a change when the variable appears on the left side of an equation, even if the value remains the same.Ariose
re-render variables ? what do mean by that ? i ll pull them in from env. Any time a submit button was pressed on the page the leaflet map flickered like a reload ...Fezzan
@mangrove109 The second argument of useMemo is an array of variables that should be watched for changes. Those are the ones I called "re-render variables." It might be best to ask a new question so other people can be helped with the solution to your problem.Ariose
Thanks for that explanation, it worked for me. But I have an issue. The markers are showing like an image failed to upload. Did anyone get this same issue? Any pointers? ThanksLallation
@Kmelow I had the same issue, but did not solve it. Please try writing a new question and maybe link to it from here.Ariose
This works like a charm! The marker didn't show up, so one additional thing I needed to do is to use github.com/ghybs/leaflet-defaulticon-compatibilityForaminifer
C
10

window is not available in SSR, you probably get this error on your SSR env.

One way to solve this is to mark when the component is loaded in the browser (by using componentDidMount method), and only then render your window required component.

class MyComp extends React.Component {
  state = {
    inBrowser: false,
  };

  componentDidMount() {
    this.setState({ inBrowser: true });
  }

  render() {
    if (!this.state.inBrowser) {
      return null;
    }

    return <YourRegularComponent />;
  }
}

This will work cause componentDidMount lifecycle method is called only in the browser.

Edit - adding the "hook" way

import { useEffect, useState } from 'react';

const MyComp = () => {
  const [isBrowser, setIsBrowser] = useState(false);
  useEffect(() => {
    setIsBrowser(true);
  }, []);

  if (!isBrowser) {
    return null;
  }

  return <YourRegularComponent />;
};

useEffect hook is an alternative for componentDidMount which runs only inside the browser.

Cornetcy answered 29/8, 2019 at 7:43 Comment(0)
M
9

Any component importing from leaflet or react-leaflet should be dynamically imported with option ssr false.

import dynamic from 'next/dynamic';

const MyAwesomeMap = dynamic(() => import('components/MyAwesomeMap'), { ssr: false });

Leaflet considers it outside their scope, as they only bring support on issues happening in vanilla JS environment, so they won't fix (so far)

Mayes answered 7/6, 2021 at 13:54 Comment(1)
This solution works for me but also have one note: You should consider not putting Map components inside 'pages' directory. I made such a mistake and was struggling for a few days until moved them out from 'pages' folder.Euchromatin
L
1

Create file loader.js, place the code below :

export const canUseDOM = !!(
    typeof window !== 'undefined' &&
            window.document &&
            window.document.createElement
    );

if (canUseDOM) {
    //example how to load jquery in next.js;
    window.$ = window.jQuery = require('jquery');
}

Inside any Component or Page import the file

import {canUseDOM} from "../../utils/loader";
{canUseDOM && <FontAwesomeIcon icon={['fal', 'times']} color={'#4a4a4a'}/>}

or Hook Version

import React, {useEffect, useState} from 'react';

function RenderCompleted() {

    const [mounted, setMounted] = useState(false);

    useEffect(() => {
        setMounted(true)

        return () => {
            setMounted(false)
        }
    });

    return mounted;
}

export default RenderCompleted;

Invoke Hook:

const isMounted = RenderCompleted();

{isMounted && <FontAwesomeIcon icon={['fal', 'times']} color={'#4a4a4a'}/>}

Loisloise answered 9/3, 2020 at 14:54 Comment(0)
S
1

Tiles are showing incorrectly because you miss leaflet.css. Download it from official website and just add to your project. Suddendly the map tiles will be shown correctly. Well, you won't see anything because default container size is 0,0. To fix it give a size to the container adding this on your styles.css:

 .leaflet-container {
    height: 450px;
    width: 100%;
}
Sketch answered 17/9, 2020 at 9:9 Comment(0)
C
1

most effective approach is suggested by @FlippingBinary I just need to argue to use little different useMemo functionality. Since useMemo is not consistent to prevent re-render when is used inside function you have to use it separately. This only works form me

vloz.tsx === Page

import React, { Suspense, lazy } from 'react';

const Vloz: React.FC = () => {
  const [position, setPosition] = useState<number[] | undefined>();
  const AddAd = lazy(() => import('@components/maps/AddAd'));

  const MemoMap = React.useMemo(() => {
    return (
      <Suspense fallback={<p>Loading</p>}>
        <AddAd position={position as LatLngTuple} setPosition={setPosition} />
      </Suspense>
    );
  }, [position]);

  return (
    <div>
      {MemoMap}
    </div>
  );
};

other component is normally used as other tutorials suggest. This approach will keep Map component render til position change

Cetane answered 3/2, 2021 at 10:32 Comment(0)
A
0
let divIcon, Map, TileLayer, Marker, Popup;
// first you must check your environment
if (process.browser) {
  // then you can import
  divIcon = require('leaflet').divIcon;
  Map = require('react-leaflet').Map;
  TileLayer = require('react-leaflet').TileLayer;
  Marker = require('react-leaflet').Marker;
  Popup = require('react-leaflet').Popup;
}
Alyssa answered 30/6, 2020 at 14:34 Comment(0)
P
0

In case anyone has a situation that the map component is shared between all pages and you have problem that map component re-renders when route changes (when switching between pages) you must load your map component in _app.js (using techniques explained in the accepted answer) and also consider using shallow routing

Parishioner answered 4/10, 2021 at 19:6 Comment(0)
L
0

We need to use this component without ssr. In this, we will make use of the next js dynamic feature. If you do a set state operation on the map with event handlers, for example, when you click the marker, the map will be rerender. You will need to use react.useMemo for avoid unnecessary re-rendering.

const MapWithNoSSR = React.useMemo(() => dynamic(() => import('../components/Map'), {
        loading: () => <p>A map is loading</p>,
        ssr: false,
    }), []);
Lied answered 1/11, 2022 at 6:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.