Why is my `client-only` component in nuxt complaining that `window is not defined`?
Asked Answered
R

7

23

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
  },
//...

Ridglea answered 15/12, 2019 at 19:34 Comment(3)
the component isn't the problem here, it's the inclusion of leaflet which is not ssr friendly. all <client-only> is doing is preventing the rendering during ssr, it is not preventing the inclusion of the script.Samoyedic
I see. Any way to use a plugin like nuxt-leaflet only for this component?Ridglea
No, because even with dynamic imports it would still be transpiled in during ssr. I would assume the inclusion of the library has to happen within a conditionalSamoyedic
R
18

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>
Ridglea answered 16/12, 2019 at 11:36 Comment(6)
Your proposed syntax was not accepted in my case, but I modified it slightly and then it is working correctly. I modified it to this: Map: () => process.client ? import('@/components/Map') : null,Pyroelectric
You should be registering the map component from a plugin with mode 'client' instead. nuxtjs.org/guide/plugins#client-or-server-side-onlyHog
@Hog I want it loaded only when necessary. Plugins are loaded globally AFAIK.Ridglea
The Map component has an export default. Do you know how to import destructed components as well? Does the import function provide this functionality?Saucer
@Hog nope, if the component/library only needs to be local, please keep it so. No need to bring it in the global scope. Simple example, you use a library for a datepicker on a single /contact-us page. Loading it with a plugin means that this datepicker will be loaded at the start of the app, whenever you use it on a given page or not. Not a good approach performance-wise.Elbrus
for me, I needed to destructure the import. So I used the following syntax Map : () => process.client ? import('package-name').then(e => e.Map) : undefinedKoopman
G
9
    <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:

  1. Rename the "components" folder to something else to stop the automatic import and then use the solution above (process.client).

  2. (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>
Graphy answered 4/12, 2020 at 13:6 Comment(1)
You can disable the auto import of the components if you find it annoying: components: false in nuxt.config.js. Otherwise, here is the article related to all of those component improvements: nuxtjs.org/tutorials/…Elbrus
A
8

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/

Amherst answered 25/2, 2020 at 5:39 Comment(0)
E
3

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.

Elbrus answered 14/4, 2022 at 9:32 Comment(2)
Re: Nuxt plugin: Is there still a problem if you were to use client-side plugins?Cinder
@MarsAndBack you can use client-side plugins with Nuxt, you'll just get some errors with Nuxt if you don't explicitly say: "I want to use them ONLY on client side". Not an issue overall.Elbrus
A
1

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.sstatic.net/kc6q4.png
Alexandretta answered 17/7, 2021 at 22:21 Comment(0)
L
0

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).

Lawgiver answered 5/6, 2021 at 21:33 Comment(1)
Using Vue.use() would make it global, so there is no benefit over using it as a plugin in your case. Prefer keeping it locally import for the given component.Elbrus
L
0

In Nuxt 2, you can use the following "double trick":

<client-only v-if="$root.$el">
  <client-only>
    <map/>
  </client-only>
</client-only>

It works because $root.$el has value only on the client side and the second <client-only> is to suppress the client-server mismatch error. The first <client-only> can be replaced by any other element, but using it has the benefit of not producing a wrapping element.

In Nuxt 3, You can create a $env plugin like so:

// plugins/env.js
export default defineNuxtPlugin(nuxtApp => {
  nuxtApp.provide('env', {
    isClientSide: process.client
  });
});

and use it:

<client-only v-if="$env.isClientSide">
  <client-only>
    <map/>
  </client-only>
</client-only>
Lobe answered 16/8, 2023 at 21:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.