Leaflet: map is not removed completely on `map.remove()`: `Uncaught TypeError: Cannot read property '_leaflet_pos' of undefined`

How to reproduce

  • Leaflet version I’m using: 1.2.0
  • Browser (with version) I’m using: Chrome Version 60.0.3112.113
  • It works fine in Firefox and Safari (havn’t tested in IE, Edge)
  • OS/Platform (with version) I’m using: macOS Sierra
  • add map in div element and add layer
this.leafletMap = new L.Map( <element> , {
            zoomControl: true, 
            dragging: this.isInDragMode, 
            touchZoom: false,
            scrollWheelZoom: false,
            doubleClickZoom: false,
            tap: false,
}
L.tileLayer( 'http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
            attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
                } ).addTo( this.leafletMap );
  • Remove the map on some user action
if (this.leafletMap ){
        this.leafletMap.eachLayer(function(layer){
            layer.remove();
        });
        this.leafletMap.remove();
        this.leafletMap = null;
    }

What behaviour I’m expecting and which behaviour I’m seeing

  • Post removal of the map, it removes the map from the element, however, if I do double click on the div, it throws error - Uncaught TypeError: Cannot read property '_leaflet_pos' of undefined It seems like DOM element is still holding the event listeners even though the map and layers are removed.

Minimal example reproducing the issue

  • this example is as simple as possible
  • this example does not rely on any third party code

Using http://playground-leaflet.rhcloud.com/ or any other jsfiddle-like site.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 11
  • Comments: 39 (13 by maintainers)

Most upvoted comments

@spydmobile here goes, this is what i did in a slightly modified form: I have no idea how to post code properly in this f*ing comment field, sorry bout that. I have edited my own comment about 10 times now lol

function removeMap()
{
    var leafletCtrl = get_your_own_leaflet_reference_from_somewhere(), 
    dom = leafletCtrl.getReferenceToContainerDomSomehow(); 

    //This removes most of the events
    leafletCtrl.off();

//After this, the dom element should be good to reuse, unfortunatly it is not
    leafletCtrl.remove(); 

    var removeDanglingEvents = function(inputObj, checkPrefix)
    {
        if(inputObj !== null)
        {
            //Taken from the leaflet sourcecode directly, you can search for these constants and see how those events are attached, why they are never fully removed i don't know
            var msPointer = L.Browser.msPointer,
            POINTER_DOWN =   msPointer ? 'MSPointerDown'   : 'pointerdown',
            POINTER_MOVE =   msPointer ? 'MSPointerMove'   : 'pointermove',
            POINTER_UP =     msPointer ? 'MSPointerUp'     : 'pointerup',
            POINTER_CANCEL = msPointer ? 'MSPointerCancel' : 'pointercancel';

            for(var prop in inputObj)
            {
                var prefixOk = checkPrefix ? prop.indexOf('_leaflet_') !== -1 : true, propVal; //if we are in the _leaflet_events state kill everything, else only stuff that contains the string '_leaflet_'
                if(inputObj.hasOwnProperty(prop) && prefixOk)
                {
                    //Map the names of the props to the events that were really attached => touchstart equals POINTER_DOWN etc
                    var evt = []; 
                    if(prop.indexOf('touchstart') !== -1) //indexOf because the prop names are really weird 'touchstarttouchstart36' etc
                    {
                        evt = [POINTER_DOWN];
                    }
                    else if(prop.indexOf('touchmove') !== -1)
                    {
                        evt = [POINTER_MOVE];
                    }
                    else if(prop.indexOf('touchend') !== -1)
                    {
                        evt = [POINTER_UP, POINTER_CANCEL];
                    }

                    propVal = inputObj[prop];
                    if(evt.length > 0 && typeof propVal === 'function')
                    {
                        evt.each(function(domEvent)
                        {
                            dom.removeEventListener(domEvent, propVal, false);
                        });                    
                    }

                    //Reference B-GONE, Garbage b collected.
                    inputObj[prop] = null;
                    delete inputObj[prop];
                }
            }
        }        
    };

    removeDanglingEvents(dom._leaflet_events, false);
    removeDanglingEvents(dom, true);
}

Hi there – I think I’m experiencing this issue as well. Here’s my basic use-case:

I’m building a viewer component (using the Leaflet-IIIF plugin, but I don’t think that impacts anything here) for objects with multiple pages / surfaces as opposed to displaying an actual map. When the viewer loads, there is a series of thumbnails which the user can click to update which view of an object is displayed in the central area of the UI.

When the user changes the view, I’m calling map.remove() before setting up a new map for the new view. The new map is created on the same DOM element as the old one (a div with an ID), and I’m not modifying the DOM in any way outside of Leaflet.

On the initial view everything works fine. But after calling map.remove() and showing a new view, the console complains: Cannot read property '_leaflet_pos' of undefined whenever the map is dragged or zoomed.

I can try to post a minimal example at some point, but this seems to be the same problem. This error comes up in Chrome but not in Firefox.

I found another workaround. Initializing your map with the undocumented option touchExtend : false deactivates the problematic handler, so no more exceptions. I don’t really know what features i’m losing by doing that but looking at the code it could be some extended gestures for mobile or touch screens?? In any case in my app everything seems to work just fine.

Hi. I have reproduced this error in a fiddle. simply put, if you create a map inside a div element, then use the remove method, then repopulate the map on the same div, every map move will then generate an error Uncaught TypeError: Cannot read property ‘_leaflet_pos’ of undefined.

To reproduce, open my fiddle, click remove map, click place map, then open the console and move the map. http://jsfiddle.net/spydmobile/5hmadjnk/

Note, it only happens in Chorme, not in FF

Here’s a SSCCE: https://jsfiddle.net/0oafw694/1/

Basically, running the following code …

map = L.map('map');
map.setView(...);
map.setMaxBounds(...);
map.remove();

… leaves two event listeners attached:

moveend: (1) […]
0: Object { fn: _panInsideMaxBounds(), ctx: undefined } // from setMaxBounds

unload: (2) […]
0: Object { fn: _destroy() , ctx: {…} }
1: Object { fn: _destroyAnimProxy(), ctx: undefined }

zoomanim: (1) […]
0: Object { fn: _createAnimProxy(), ctx: undefined }

I guess, zoomanim/_createAnimProxy is handled via unload/_destroyAnimProxy, and thus no problem. But the moveend/_panInsideMaxBounds needs to be unregistered. I’ll prepare a PR…

I think i might have found the solution:

The Map container div still has some events that are fired even after map.off and map.remove.

In my case the map has properties that start with _leaflet_ and some of those functions i found to be on the map itself in property "map._leaflet_events".

Those seem to be attached as pointerdown, pointermove and such but the names of the Properties is like map._leaflet_touchstarttouchstart32 etc.

I found that if i iterate those and remove them manually (using removeEventListener then nulling and deleting the property itself for good measure), i can reuse the div for another map. This also put an end to the memory leaks i was seeing.

I cannot post code here, but if you search the leaflet source for POINTER_DOWN you’ll see the events that get attached, and know how to detach them.

Closing per above comment.

I just ended up creating a div for the map that has a dynamic id, so when I have to reuse the div, I remove() the existing map in order to release memory (even so there are some events still going arround) and then redraw the div with a diferent id so I create a new map in it.

I also store all of my maps in an object, so I can manipulate them according to its id (I have more than one map visible some times, all with dynamic ids)

@IvanSanchez I’m not sure it’s the same issue, but could be related. When you destroy the map when zoom animation is in progress, you get the same error: Uncaught TypeError: Cannot read property '_leaflet_pos' of undefined.

I tried to look inside the code and found out that inside Map._animateZoom() there is a line: setTimeout(Util.bind(this._onZoomTransitionEnd, this), 250); If I understand it enough, this timeout is not destroyed when map is removed, so the function Map._onZoomTransitionEnd is always called. It can be your “missing a function call here or there”.

And the simplified call tree this._onZoomTransitionEnd -> this._move -> this._getNewPixelOrigin -> this._getMapPanePos -> getPosition(this._mapPane) -> return el._leaflet_pos fails, because this._mapPane is undefined.

Maybe this case could be fixed, if you wrap the this._move and this._moveEnd calls into the if (this._mapPane) {} condition, but I didn’t test if it has some other consequences.

Replace this:

_onZoomTransitionEnd: function () {
	if (!this._animatingZoom) { return; }

	if (this._mapPane) {
		removeClass(this._mapPane, 'leaflet-zoom-anim');
	}

	this._animatingZoom = false;

	this._move(this._animateToCenter, this._animateToZoom);

	// This anim frame should prevent an obscure iOS webkit tile loading race condition.
	requestAnimFrame(function () {
		this._moveEnd(true);
	}, this);
}

with this:

_onZoomTransitionEnd: function () {
	if (!this._animatingZoom) { return; }

	this._animatingZoom = false;

	if (this._mapPane) {
		removeClass(this._mapPane, 'leaflet-zoom-anim');
		this._move(this._animateToCenter, this._animateToZoom);

		// This anim frame should prevent an obscure iOS webkit tile loading race condition.
		requestAnimFrame(function () {
			this._moveEnd(true);
		}, this);
	}
}

i know that function and i see it gets called, but not for all events.

Now the question is: What are the events for which removePointerListener is not called? Maybe we’re missing a function call here or there.

Also should those “pointer” events even be attached on Windows 10 without Touchscreen and in Chrome?

Yes. It’s nearly impossible to detect whether a system has a touchscreen, so if the browser supports pointer events, the assumption is that they will be used.

i don’t know enough about leaflets inner workings to provide a real fix 😦

Hey, don’t despair, this is great investigation work! 😄

Yeah, i know that function and i see it gets called, but not for all events.

I think the problem is that the code attaches them as “pointermove” etc to the dom, but the property names are “touchstart” etc. Also the word “touchstart” is seen twice in the property name, maybe an unexpected doube concat of the id and event name?

Also should those “pointer” events even be attached on Windows 10 without Touchscreen and in Chrome? Unfortunatly, i don’t know enough about leaflets inner workings to provide a real fix 😦

@FLoibl This is a very good investigation 👍

Could you please add some logging around… ? https://github.com/Leaflet/Leaflet/blob/5161140e952969c5da27751b79154a2c93f53bfa/src/dom/DomEvent.Pointer.js#L39 and https://github.com/Leaflet/Leaflet/blob/fe9e0f2333888e8c02b9e7f83bf337d91006ed0a/src/dom/DomEvent.js#L133

Those should be running for every event when a L.Map is destroyed, and should be doing the same thing that you’re doing, but I wonder why it doesn’t work as expected.

Yes spydmobile thank you for the example, this is the same error I am seeing in Chrome as I reported above