Leaflet with next.js?
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.
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='© <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.
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)
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'}/>}