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

15,411

Solution 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

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

Solution 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

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
Share:
15,411
yam
Author by

yam

Updated on June 05, 2022

Comments

  • yam
    yam almost 2 years

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