'Why is my `client-only` component in nuxt complaining that `window is not defined`?
I have Vue SPA that I'm trying to migrate to nuxt. I am using vue2leaflet in a component that I enclosed in <client-only> tags but still getting an error from nuxt saying that window is not defined.
I know I could use nuxt-leaflet or create a plugin but that increases the vendor bundle dramatically and I don't want that. I want to import the leaflet plugin only for the components that need it. Any way to do this?
<client-only>
<map></map>
</client-only>
And the map component:
<template>
<div id="map-container">
<l-map
style="height: 80%; width: 100%"
:zoom="zoom"
:center="center"
@update:zoom="zoomUpdated"
@update:center="centerUpdated"
@update:bounds="boundsUpdated"
>
<l-tile-layer :url="url"></l-tile-layer>
</l-map>
</div>
</template>
<script>
import {
LMap,
LTileLayer,
LMarker,
LFeatureGroup,
LGeoJson,
LPolyline,
LPolygon,
LControlScale
} from 'vue2-leaflet';
import { Icon } from 'leaflet';
import 'leaflet/dist/leaflet.css';
// this part resolve an issue where the markers would not appear
delete Icon.Default.prototype._getIconUrl;
export default {
name: 'map',
components: {
LMap,
LTileLayer,
LMarker,
LFeatureGroup,
LGeoJson,
LPolyline,
LPolygon,
LControlScale
},
//...
Solution 1:[1]
I found a way that works though I'm not sure how. In the parent component, you move the import statement inside component declarations.
<template>
<client-only>
<map/>
</client-only>
</template>
<script>
export default {
name: 'parent-component',
components: {
Map: () => if(process.client){return import('../components/Map.vue')},
},
}
</script>
Solution 2:[2]
<template>
<client-only>
<map/>
</client-only>
</template>
<script>
export default {
name: 'parent-component',
components: {
Map: () =>
if (process.client) {
return import ('../components/Map.vue')
},
},
}
</script>
The solutions above did not work for me.
Why? This took me a while to find out so I hope it helps someone else.
The "problem" is that Nuxt automatically includes Components from the "components" folder so you don't have to include them manually. This means that even if you load it dynamically only on process.client it will still load it server side due to this automatism.
I have found the following two solutions:
Rename the "components" folder to something else to stop the automatic import and then use the solution above (process.client).
(and better option IMO) there is yet another feature to lazy load the automatically loaded components. To do this prefix the component name with "lazy-". This, in combination with will prevent the component from being rendered server-side.
In the end your setup should look like this Files:
./components/map.vue
./pages/index.html
index.html:
<template>
<client-only>
<lazy-map/>
</client-only>
</template>
<script>
export default {
}
</script>
Solution 3:[3]
The <client-only> component doesn’t do what you think it does. Yes, it skips rendering your component on the server side, but it still gets executed!
https://deltener.com/blog/common-problems-with-the-nuxt-client-only-component/
Solution 4:[4]
Here is how I do it with Nuxt in Universal mode: this will: 1. Work with SSR 2. Throw no errors related to missing marker-images/shadow 3. Make sure leaflet is loaded only where it's needed (meaning no plugin is needed) 4. Allow for custom icon settings etc 5. Allow for some plugins (they were a pain, for some reason I thought you could just add them as plugins.. turns out adding them to plugins would defeat the local import of leaflet and force it to be bundled with vendors.js)
Wrap your template in <client-only></client-only>
<script>
let LMap, LTileLayer, LMarker, LPopup, LIcon, LControlAttribution, LControlZoom, Vue2LeafletMarkerCluster, Icon
if (process.client) {
require("leaflet");
({
LMap,
LTileLayer,
LMarker,
LPopup,
LIcon,
LControlAttribution,
LControlZoom,
} = require("vue2-leaflet/dist/vue2-leaflet.min"));
({
Icon
} = require("leaflet"));
Vue2LeafletMarkerCluster = require('vue2-leaflet-markercluster')
}
import "leaflet/dist/leaflet.css";
export default {
components: {
"l-map": LMap,
"l-tile-layer": LTileLayer,
"l-marker": LMarker,
"l-popup": LPopup,
"l-icon": LIcon,
"l-control-attribution": LControlAttribution,
"l-control-zoom": LControlZoom,
"v-marker-cluster": Vue2LeafletMarkerCluster,
},
mounted() {
if (!process.server) //probably not needed but whatever
{
// This makes sure the common error that the images are not found is solved, and also adds the settings to it.
delete Icon.Default.prototype._getIconUrl;
Icon.Default.mergeOptions({
// iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'), // if you want the defaults
// iconUrl: require('leaflet/dist/images/marker-icon.png'), if you want the defaults
// shadowUrl: require('leaflet/dist/images/marker-shadow.png') if you want the defaults
shadowUrl: "/icon_shadow_7.png",
iconUrl: "/housemarkerblue1.png",
shadowAnchor: [10, 45],
iconAnchor: [16, 37],
popupAnchor: [-5, -35],
iconSize: [23, 33],
// staticAnchor: [30,30],
});
}
},
And there's proof using nuxt build --modern=server --analyze
https://i.stack.imgur.com/kc6q4.png
Solution 5:[5]
Answers here are more focused towards import the Map.vue component while the best approach is probably to properly load the leaflet package initially inside of Map.vue.
Here, the best solution would be to load the components like so in Map.vue
<template>
<div id="map-container">
<l-map style="height: 80%; width: 100%">
<l-tile-layer :url="url"></l-tile-layer>
</l-map>
</div>
</template>
<script>
import 'leaflet/dist/leaflet.css'
export default {
name: 'Map',
components: {
[process.client && 'LMap']: () => import('vue2-leaflet').LMap,
[process.client && 'LTileLayer']: () => import('vue2-leaflet').LTileLayer,
},
}
</script>
I'm not a leaflet expert, hence I'm not sure if Leaflet care if you import it like import('vue2-leaflet').LMap but looking at this issue, it looks like it doesn't change a lot performance-wise.
Using Nuxt plugins is NOT a good idea as explained by OP because it will increase the whole bundle size upfront. Meaning that it will increase the loading time of your whole application while the Map is being used only in one place.
My How to fix navigator / window / document is undefined in Nuxt answer goes a bit more in detail about this topic and alternative approaches to solve this kind of issues.
Especially if you want to import a single library like vue2-editor, jsplumb or alike.
Solution 6:[6]
I am replicating my answer here since this is the first post that gets reached searching for this kind of problem, and using the solutions above still caused nuxt to crash or error in my case.
You can import your plugin in your mounted hook, which should run in the client only. So:
async mounted() {
const MyPlugin = await import('some-vue-plugin');
Vue.use(MyPlugin);
}
I do not know about the specific plugin you are trying to use, but in my case I had to call Vue.use() on the default property of the plugin, resulting in Vue.use(MyPlugin.default).
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | kissu |
| Solution 2 | Flobe |
| Solution 3 | |
| Solution 4 | |
| Solution 5 | kissu |
| Solution 6 | kissu |
