These aren’t the DART event listeners you are looking for

November 18, 2015

I am a development manager for one of our development teams, which was recently formed by merging two others. While originally a vanilla js application, the project was rewritten in DARTTM just a few weeks before I joined. The project largely has a great codebase to work on, but occasionally I find something goofy leftover from the DART conversion. This is a story about one of those things, or so I thought.

The scenario

Yesterday I was working on a simple issue where the browser console was filling with the same repetitive error under a certain scenario:

Uncaught Error: Invariant Violation: Component (with keys: getDOMNode,props,context,state,refs,_reactInternalInstance) contains `render` method but is not mounted in the DOM

This was occurring when I opened a document in our application, closed it, and then moved my mouse around the space where the renderer had previously been mounted. Being a React component, it seemed that the renderer was still executing code even though it was no longer mounted in the DOM. A quick traversal through the relevant DART code quickly identified the culprit:

document.addEventListener('keydown', _handleKeyDown);
document.addEventListener('keypress', _handleKeyPress);
document.addEventListener('mouseup', _handleMouseUp);
document.addEventListener('mousemove', _handleMouseMove);
document.addEventListener('paste', _handlePaste);
document.addEventListener('copy', _handleCopy);
document.addEventListener('cut', _handleCut);

We added seven distinct DART event listeners when we mounted the component, but only removed six of them when we unmounted it:

document.removeEventListener('keydown', _handleKeyDown);
document.removeEventListener('keypress', _handleKeyPress);
document.removeEventListener('paste', _handlePaste);
document.removeEventListener('copy', _handleCopy);
document.removeEventListener('cut', _handleCut);
document.removeEventListener('mouseup', _handleMouseUp);

Note that we forgot to remove the _handleMouseMove listener we had previously added. Wow! I thought this was going to be the simplest ticket that I have worked on in a long time. I added the missing line to the above block, rebuilt, and expected to have my problem disappear.

Except, it did not. The behavior was exactly as before. In fact, as I wiggled my mouse around and saw my console fill with the same error, I also noticed that typing still fired that _handleKeyDown listener as well. It would seem all seven listeners were still alive and listening. Apparently, those DART event listeners were not the ones I was expecting.

The problem

We were clearly removing seven listeners, and those listeners were legit, non-null functions. They had the same name as the ones we originally added. Every OO bone in my body was pretty sure this made no sense.

The only thing that did make sense was that these new functions were bold imposters, so I decided to expose their fraud. Remembering the id() function in Python, I found a similar construct in DART: identityHashCode(). Using this, I found that the identities of the listeners we added to the document were different than the ones we removed. But they were the same functions!

At this point, I was quite confused and chatted with a few fellow devs (thanks to Dominic Frost, Ben Echols, and Rob Becker). Our general working consensus was that either the React lifecycle and its virtual dom were somehow tied up in this mess, or the way DART referred to these named functions wasn't what we expected.

So I wrote a small DART app that could re-create this problem without involving React. The issue then became more clear. Check out the gist, and you will quickly learn: In DART, class functions have different identities in every context they are referred to.

If you refer to a class function in multiple places, it will have multiple identities. While not normally an issue (they will all resolve to the correct location when invoked, for example), their failure to evaluate to identical meant that the DART event listeners we added to the document were, effectively, not the ones we were attempting to remove. Browsers are pretty blunt about this—your references to functions when calling addEventListener and removeEventListener must be identical. Now that we understood the problem, finding references to it was far easier:

So there you have it—finally an answer! Within the closure of a DART function, the technical identity of a class function couldn't be easily canonicalized. While there was a decent amount of complaints in this issue thread, at least the problem was clear.

The resolution

During my attempts to understand this problem originally, I found that a class-level variable reference to the listener functions, being defined above these individual closures, would circumvent the problem. That was a solution that was largely being sold in the GITHUB® thread linked above as well. So that is what I decided to do for our particular bug. I now lazily build up a map of these listener functions at the class level:

Map get listeners {
    if (listenerMap == null) {
      listenerMap = {
        'keydown': _handleKeyDown,
        'keypress': _handleKeyPress,
        'mouseup': _handleMouseUp,
        'mousemove': _handleMouseMove,
        'paste': _handlePaste,
        'copy': _handleCopy,
        'cut': _handleCut
      };
    }
    return listenerMap;
}

And I use those when we want to add/remove these event listeners in our React lifecycle:

// add event listeners 
document.addEventListener('keydown', listeners['keydown']);
document.addEventListener('keypress', listeners['keypress']);
document.addEventListener('mouseup', listeners['mouseup']);
document.addEventListener('mousemove', listeners['mousemove']);
document.addEventListener('paste', listeners['paste']);
document.addEventListener('copy', listeners['copy']);
document.addEventListener('cut', listeners['cut']);

...

// remove event listeners
document.removeEventListener('keydown', listeners['keydown']);
document.removeEventListener('keypress', listeners['keypress']);
document.removeEventListener('paste', listeners['paste']);
document.removeEventListener('copy', listeners['copy']);
document.removeEventListener('cut', listeners['cut']);
document.removeEventListener('mouseup', listeners['mouseup']);
document.removeEventListener('mousemove', listeners['mousemove']);

Because I am adding event listeners by an explicit variable reference, DART is able to see that they are indeed references to the identical functions. The ambiguity of our original code is gone, and now I am confident that these are, in fact, the DART event listeners I am looking for.

© 2012 Google Inc. All rights reserved. DART is a trademark of Google Inc.

GITHUB® is an exclusive trademark registered in the United States by GitHub, Inc.