treecon
treecon

Reputation: 2835

Uncaught TypeError: this._map is null (Vue.js 3, Leaflet)

I am getting a strange error from Leaflet in a Vue.js project (version 3).

If I close a popup and zoom in/out, this error occurs on Firefox:

Uncaught TypeError: this._map is null

And on Chrome:

Cannot read property '_latLngToNewLayerPoint' of null

The map component is as follows:

<template>
  <div id="map"></div>
</template>

<script>
import "leaflet/dist/leaflet.css";
import L from 'leaflet';

export default {
  name: 'Map',
  data() {
    return {
      map: null
    }
  },
  mounted() {
    this.map = L.map("map").setView([51.959, -8.623], 12);
    L.tileLayer("https://{s}.tile.osm.org/{z}/{x}/{y}.png", {
        attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(this.map);

    L.circleMarker([51.959, -8.623]).addTo(this.map)
      .bindPopup('I am a marker')
      .openPopup();
  }
}
</script>

<style scoped>
  #map {
    height: 300px;
    width: 100%;
  }
</style>

How to reproduce the error:

  1. Open stackblitz: https://stackblitz.com/edit/vue-gjeznj
  2. Close popup
  3. Zoom in/out

Can it be just a bug? Or is there any error in code that I missed?

Upvotes: 13

Views: 4287

Answers (4)

Soham
Soham

Reputation: 1

Well, considering the migration to Composition Api and the script setup (in my case as default), the solution that works for me is the one proposed by @Keith with shallowref. Also would like to mention am using the leaflef (1.9.4) with leaflet.markercluster (1.5.3) in a Quasar(2.16.0) project (Quasar is a macroframerok on top of Vue (3.4.18)).

const map = shallowRef();

Then using the map.value as the instance...

In other projects with just Vue 3.x was not neccesary.

Also would like to add to Leaflet's issues notes in Vue projects, that depending on wich libraries are been used, may require to add the following line, (e.g if using vue-leaflet)

globalThis.L = L;

Upvotes: 0

Keith
Keith

Reputation: 339

The answer from @ghybs is completely correct, it is caused by deep proxying of refs.

An alternative solution would be to simply use a shallowRef whenever your ref contains Leaflet data. AFAIK, it doesn't look like there is a way to create a shallowRef if we use the Options API. So here is a version of your code using composition API, with a shallowRef:

<template>
  <div id="map"></div>
</template>

<script setup>
import "leaflet/dist/leaflet.css";
import L from 'leaflet';
import { shallowRef, onMounted } from 'vue';

const map = shallowRef();

onMounted(() => {
  map.value = L.map("map").setView([51.959, -8.623], 12);
  L.tileLayer("https://{s}.tile.osm.org/{z}/{x}/{y}.png", {
    attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
  }).addTo(map.value);

  L.circleMarker([51.959, -8.623]).addTo(map.value)
    .bindPopup('I am a marker')
    .openPopup();
  }
)
</script>

<style scoped>
  #map {
    height: 300px;
    width: 100%;
  }
</style>

Upvotes: 7

ghybs
ghybs

Reputation: 53370

FWIW, this seems a new issue since Vue 3.

The problem is absent from Vue version 2 with Leaflet: https://codesandbox.io/s/fast-firefly-lqmwm?file=/src/components/HelloWorld.vue

Just to make sure, here is a reproduction of the issue with the same code but Vue version 3, on CodeSandbox: https://codesandbox.io/s/laughing-mirzakhani-sgeoq?file=/src/components/HelloWorld.vue

What seems to be the culprit is the proxying of this.map by Vue, which seems to interfere with Leaflet events (un)binding. It looks like Vue 3 now automatically performs deep proxying, whereas Vue 2 was shallow.

As described in https://v3.vuejs.org/api/basic-reactivity.html#markraw:

[...] the shallowXXX APIs below allow you to selectively opt-out of the default deep reactive/readonly conversion and embed raw, non-proxied objects in your state graph. They can be used for various reasons:

  • Some values simply should not be made reactive, for example a complex 3rd party class instance, or a Vue component object.

...which is the case of Leaflet built map object.

A very simple workaround would be not to use this.map (i.e. not to store the Leaflet built map object in the component state, to prevent Vue from proxying it), but to just store it locally (e.g. const map = L.map() and then myLayer.addTo(map)).

But what if we do need to store the map object, typically so that we can re-use it later on, e.g. if we want to add some Layers on user action?

Then make sure to properly unwrap / unproxy this.map before using it with Leaflet, e.g. using Vue 3 toRaw utility function:

Returns the raw, original object of a reactive or readonly proxy. This is an escape hatch that can be used to temporarily read without incurring proxy access/tracking overhead or write without triggering changes.

import { toRaw } from "vue";

export default {
  name: "Map",
  data() {
    return {
      map: null,
    };
  },
  mounted() {
    const map = L.map("map").setView([51.959, -8.623], 12);
    L.tileLayer("https://{s}.tile.osm.org/{z}/{x}/{y}.png", {
      attribution:
        '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
    }).addTo(map);

    L.circleMarker([51.959, -8.623])
      .addTo(map)
      .bindPopup("I am a marker")
      .openPopup();

    this.map = map;
  },
  methods: {
    addCircleMarker() {
      L.circleMarker([
        51.959 + Math.random() * 0.05,
        -8.623 + Math.random() * 0.1,
      ])
        .addTo(toRaw(this.map)) // Make sure to "unproxy" the map before using it with Leaflet
        .bindPopup("I am a marker")
        .openPopup();
    },
  },
}

Demo: https://codesandbox.io/s/priceless-colden-g7ju9?file=/src/components/HelloWorld.vue

Upvotes: 17

treecon
treecon

Reputation: 2835

Having read arieljuod's link, it seems that the only option, without tweaking Leaflet's js. file, is to disable zoom animations.

this.map = L.map("map", {zoomAnimation: false})

If animations are needed, a minor tweak in Leaflet's js file is proposed here: https://salesforce.stackexchange.com/a/181000

Upvotes: 4

Related Questions