Leaflet: map is not removed completely on map.remove() from the DOM

Created on 13 Sep 2017  ·  37Comments  ·  Source: Leaflet/Leaflet

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.

needs more info

Most helpful comment

@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);
}

All 37 comments

Hi and thanks for taking the time to report this bug.

However, it appears something is missing from the steps you describe to reproduce the issue. I set up a playground example as well as I could along the steps above: http://playground-leaflet.rhcloud.com/rezop/edit?html,output

In this example, I can not see any errors after the map is removed, so apparently there's something more going on that causes the problem you're seeing. Could you please provide more detail, so we can track this down.

@perliedman Thanks for quick response and putting together playground example. I am not able to reproduce on that one. Trying few more scenarios. while I am still looking into it, a couple of questions -

Do I need explicit Layer removing, if I am doing map.remove()? my guess is, it will take care of layer removal as well, but can you confirm.

And the reason you have map.remove() under timeout is that in the example you are destroying map right after creation, otherwise it's not required to enclosed with a timeout. correct?

Remember, when working with open source, _use_ the source! :wink: To answer your first question, _yes_, remove will remove the layers: https://github.com/Leaflet/Leaflet/blob/master/src/map/Map.js#L731

The reason I put it in a timeout was to make my example a bit more realistic, making sure the tile layer was actually initialized properly with loaded tiles, etc. The example works just as well without putting the remove call in a timeout, but it seems like a bit of an artificial test.

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.

@egardner yes, please try to create an example that reproduce this!

@egardner exact same problem on previously stable electron (Chromium + Node) app I had to revert to:
"leaflet": "1.0.0",
"leaflet.markercluster": "1.0.0-rc.1.0"
from 1.2.0 to remove error Cannot read property '_leaflet_pos' of undefined
This was also after a map.remove() before recreating the map in the same DOM element. I don't have time just now to create and example on the short term

We are also experiencing a similar issue trying to destroy a map, it seems to be holding on to references

I'll reiterate what I said above: for us to be able to do anything about this issue, please provide an example that reproduces the problem.

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

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

I'm seeing the same error but in a slightly different use case. The same error is thrown on resize due to an invalidateSize call:

Uncaught TypeError: Cannot read property '_leaflet_pos' of undefined
    at getPosition (leaflet-src.js:2765)
    at NewClass._getMapPanePos (leaflet-src.js:4378)
    at NewClass._rawPanBy (leaflet-src.js:4180)
    at NewClass.invalidateSize (leaflet-src.js:3509)
    at NewClass.<anonymous> (leaflet-src.js:4244)

The full call stack starts at the _onResize handler. I am using react-leaflet but no part of the stack trace points to that or the local code being the issue. I did try a few older versions (e.g. 1.0.3 and 1.2.0) thinking we may at least be able to lock it down to a specific 1.x.x version, but had no luck.

Is there any update on this? Having the same Problem here integrating leaflet into my application. After the map is destroyed, the dom element still has an _leaflet_events property, but getting rid of this object didn't help either.

It seems that the context of the handler function is stale (the _mapPane private property in the handler args points to a non-existant element).

I'm experiencing this too. This is one of the thrown exceptions I'm seeing:

https://sentry.io/share/issue/b414c58ea85c44ee9e0e40ad0781883a/

It seems that this mostly happens when a user uses the browsers back button to leave the map.

I think i might have found the solution:

The Map container div still has some events that are fired even after map.off andmap.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, pointermoveand 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.

I can reproduce this on Chrome but also in FF (both on my own project and with the jsfiddle provided by @spydmobile)

@FLoibl Perhaps a gist, fiddle or some snippet posted elsewhere to demonstrate the technique you are using successfully to workaround?

@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);
}

Ah triple backticks, got it, ty.

@FLoibl This is a very good investigation :+1:

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.

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

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! :smile:

This bug is not present in version 1.0.3. I picked up @spydmobile jsfiddle and change the leaflet version and the error goes away http://jsfiddle.net/5hmadjnk/47/ . With version 1.1.0 it's there already.

@benru89 This bug was in fact present in 1.0.3, in the form of https://github.com/Leaflet/Leaflet/issues/5263 (mostly fixed by https://github.com/Leaflet/Leaflet/pull/5265).

The change from 1.0.3 to 1.1.0 also affected the now deprecated L.Mixin.Events and the rollupJS build, so I don't think this can be tracked down nicely, not even with git bisect.

@IvanSanchez I compared the remove function in 1.0.3 and 1.1.0 and this was added :

for (i in this._panes) {
    remove(this._panes[i]);
}
this._layers = [];
this._panes = [];
delete this._mapPane;
delete this._renderer;

If i remove the 6th line, the one that deletes the mapPane the error goes away. I don't know however the impact of removing this, i guess the mapPane has to be deleted but this bug was certainly introduced when that line was added.

@benru89 Wow, that's also good info :+1:

I just don't see what pointer event handlers are there in map panes right now, though :thinking:

I think by not removing the map Pane, you only mask the problem. When i tracked the call stack, the problem was that some _mapPane Object was pointing to a destroyed dom element thus trying to get a cached position from undefined. If the pane is not destroyed the ghost events might go through without triggering the exception.

@Floibl I agree with you, i think it's not the solution, but i noticed the null _mapPane when checking the call stack so that's why i tried not removing that line. The solution has to be on the side of removing properly the event handlers i guess.

I think that the event handler causing this (at least in my case and @spydmobile ) is called "touchExtend" so it is a leaflet.draw handler. I found out that removing the import for leaflet.draw stop the exceptions too.

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.

@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);
    }
}

Any updates on this? I'm experiencing the same issue. touchExtend : false doesn't help. The issue happens when I navigate away from the view, where I have the map (it is being destroyed at this point by calling map.remove()) and then I navigate back to this view. It should create and initialize the new map, but I'm getting the '_leaflet_pos' error at getPosition in the setMaxBounds method:

Uncaught (in promise) TypeError: Cannot read property '_leaflet_pos' of undefined
    at getPosition (webpack-internal:///./node_modules/leaflet/dist/leaflet-src.js:2445)
    at NewClass._getMapPanePos (webpack-internal:///./node_modules/leaflet/dist/leaflet-src.js:4409)
    at NewClass._moved (webpack-internal:///./node_modules/leaflet/dist/leaflet-src.js:4413)
    at NewClass.getCenter (webpack-internal:///./node_modules/leaflet/dist/leaflet-src.js:3774)
    at NewClass.panInsideBounds (webpack-internal:///./node_modules/leaflet/dist/leaflet-src.js:3488)
    at NewClass._panInsideMaxBounds (webpack-internal:///./node_modules/leaflet/dist/leaflet-src.js:4220)
    at NewClass.setMaxBounds (webpack-internal:///./node_modules/leaflet/dist/leaflet-src.js:3444)

And also in the setView method:

Uncaught (in promise) TypeError: Cannot read property '_leaflet_pos' of undefined at getPosition (leaflet-src.js?9eb7:2445) at NewClass._getMapPanePos (leaflet-src.js?9eb7:4409) at NewClass.containerPointToLayerPoint (leaflet-src.js?9eb7:3989) at NewClass._getCenterLayerPoint (leaflet-src.js?9eb7:4446) at NewClass._getCenterOffset (leaflet-src.js?9eb7:4451) at NewClass._tryAnimatedPan (leaflet-src.js?9eb7:4526) at NewClass.setView (leaflet-src.js?9eb7:3181)

Same issue after map.remove(), reinit my map and get this exact error

Same issue with v1.6.0. It is a complicated issue

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

As per my experiments with the leaflet, whatever events (ex. moveend, movestart, etc.) that are manipulated by developers, changes their default behavior and remains in memory while unloading leaflet from the dom.
I had done this: @moveend="()=>{enableRecenter = true}" and so, the handler of 'moveend' remained in memory while unloading/removing the map.
I removed the manipulations (my own implementations) of these methods from map component itself and now this error stopped showing up.

So basically, DO NOT TOUCH THE MAP METHODS EVER!!! Off course, unless, the library detects this behavior and fixes this bug.

Was this page helpful?
0 / 5 - 0 ratings