Backbone: duplicate events when instantiating view twice

Created on 5 Feb 2012  路  9Comments  路  Source: jashkenas/backbone

When I define a view (via Backbone.View.extend) and instantiate it twice (via new), when an event happens once on each ivies object, it is fired twice.

Short example:

<html>
<head>
    <script type="text/javascript" src="js/jquery-1.7.1.js"></script>
    <script type="text/javascript" src="js/underscore-1.3.1.js"></script>
    <script type="text/javascript" src="js/backbone-0.9.1.js"></script>
</head>
<body>
    <div id="test"></div>
    <script type="text/javascript">
        testview = Backbone.View.extend ({
            el: '#test',
             events: {'click .test': 'doTest'},
             doTest: function() {alert('test')},
             render: function() {$('#test').append($('<a>').attr('href', '#').addClass('test').append('test'));}});
        view_one = new testview;
        view_two = new testview;
        view_one.render();
    </script>
    </body>
</html>

All 9 comments

Out of curiosity, why are you instantiating it twice? Are you in a mobile view or something where the first instance in the viewport is replaced with another view, and when when the user comes back to that first view, you are getting double bindings?

Something like that. I have a home view, that will call different endpoints if the user is logged in or not. The event bindings are for the login / logout buttons. Both views are instantiated but only one is rendered at the time. When they login or logout, I'll switch to the same view but in a different mode. Hope that's clear enough.

It's still not entirely clear to me, why you have both views instantiated at the same time. Having both bind to the same pre-existing element in the dom means that there will be double events. This isn't really a bug, but just the way that events and specifically event delegation works.

If you instantiate a view on a pre-existing element, you need to specifically unbind those events or they will persist until the the element node itself is removed from the dom.

Well, it seemed like a good idea to instantiate both views during initialization, bind both to the same root element in the DOM, and then render the one or the other according to the login mode and have it replace the other's elements while rendering. I guess there's a better way to do it though, from your comments.

I would instantiate views when you need them. If a view isn't being used, there is no need in having it around taking up memory and potentially introducing side effects like you are seeing.

I tend not to use this pattern:

var View = Backbone.View.extend ({
    el: '#test',
    events: 
    {
        'click .test': 'onClick'
    },
    onClick: function(event) 
    { 
        ... 
    }
});

view = new View;
view.render();

but instead I tend to use this pattern, exactly to avoid zombie views that result in duplication of events

var View = Backbone.View.extend ({
    events: 
    {
        'click .test': 'onClick'
    },
    onClick: function(event) 
    { 
        ... 
    }
});

view = new View;
$('#target').append(view.render().el);

The only location where I would use your first pattern, is in the Application's Chrome view that instantiates the GUI:

var Application = Backbone.View.extend({
    el: '#wrapper',
    initialize: function (options)
    {
        this.render();
    }
)};

$(document).ready(function () {
    window.application = new Application();
});

ditto on what @vincentbriglia said about not defining el and letting the view generate it's own element. I do that 95% of the time as well. If however you do want to define el and clean up the events when removing, you can define you own Backbone.View.destroy() method with an .off() for every corresponding on() and every event in events: {}

for example:

var MyView = Backbone.View.extend({

  initialize: function() {
    this.model.on('change', this.render, this);
  },

  render: function() {
    return this;
  },

  destroy: function() {
    this.remove();
    this.model.off('change', this.render, this);
  }
})

Thanks, @malandrew and @vincentbriglia, that really helps.

Just to update this for posterity... I added a destroy method to the View class that looks like this

           destroy: function() {
                this.undelegateEvents();
            }

Then I just destroy the current view (State.view) whenever I instantiate().

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jamiebuilds picture jamiebuilds  路  12Comments

cueedee picture cueedee  路  3Comments

rafde picture rafde  路  9Comments

zowers picture zowers  路  11Comments

jonathan picture jonathan  路  11Comments