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='&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!
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?
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='&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 && }
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='© <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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With