Search code examples
javascriptbackbone.jsrace-condition

render not getting called for one api and is for another


I have configured a simple backbone Model and View using an underscore template. The exact same configuration is used for two separate APIs.

API 1 works as expected.

To reproduce the problem, comment out the url for API 1 and uncomment the url for API 2.

As you can see I have normalized the response data for both apis, the exact same data structure is returned for both apis. However, the render method for API 2 is not called. To make matters even more strange, on rare occasions render does get called by API 2.

What am I missing here?

// Model
var Quote = Backbone.Model.extend({
  // API 1
  //urlRoot: 'http://quotes.stormconsultancy.co.uk/quotes/1.json',
  
  // API 2
  urlRoot: 'http://quotes.rest/qod.json',

  parse: function (data){
    try{
      data = data['contents'].quotes[0];
    }
    catch(e){
    }

    var rd = {author:data.author, quote:data.quote}
    console.log("parsed", typeof rd, rd);
    return rd;
  },
  
  // UPDATE as suggested by cory
  initialize: function() {
    this.on('all', function(eventName) {
      console.log('QuoteModel: ' + eventName);
    });
  }
});

// View
var QuoteView = Backbone.View.extend({
  initialize: function() {
    this.template = _.template($('#quote-template').html());
    this.listenTo(this.model, 'change', this.render);
  },

  render: function(){
    console.log("render", this.model.attributes)
    this.$el.html(this.template(this.model.attributes));
  }
});

var quoteM = new Quote();
quoteM.fetch();

$(document).ready(function() {
	var quoteV = new QuoteView({
		el: $('#quote'),
		model: quoteM
	});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.min.js"></script>

<script type="text/html" id="quote-template">
		<p>The author is : <%= author %></p>
		<p>The content is : <%= quote %></p>
</script>

<div id="quote"></div>


Solution

  • You have a race condition, where you fetch before creating the view.

    So if the fetch finishes before the document is ready, the change event gets triggered before the view has started listening to the model.

    The simplest solution

    $(document).ready(function() {
        var quoteM = new Quote();
        var quoteV = new QuoteView({
            el: $('#quote'),
            model: quoteM
        });
        // fetch after
        quoteM.fetch();
    });
    

    The best solution

    var API_DOMAIN = "http://quotes.rest/";
    // Reusable model
    var Quote = Backbone.Model.extend({});
    
    // reusable quotes collection
    var QuoteCollection = Backbone.Collection.extend({
      model: Quote,
      // simple generic parse
      parse: function(response) {
        return response.contents.quotes;
      },
    });
    
    // View
    var QuoteView = Backbone.View.extend({
      // GOOD: gets called once
      template: _.template($('#quote-template').html()),
      
      initialize: function() {
        // BAD: gets called for every view
        // this.template = _.template($('#quote-template').html());
        
        this.listenTo(this.model, 'change', this.render);
      },
    
      render: function() {
        console.log("render", this.model.attributes)
        this.$el.html(this.template(this.model.toJSON()));
        // Backbone standard for chaining
        return this;
      }
    });
    
    
    $(function() {
      var quoteV,
        collection = new QuoteCollection();
      collection.fetch({
        url: API_DOMAIN + 'qod.json',
        success: function(collection, response, options) {
          // only create the view when you need it
          quoteV = new QuoteView({
            el: $('#quote'),
            model: collection.first()
          });
          
          // manually render to be 100% in control. The event now only
          // serves if the model really changes.
          quoteV.render();
        }
      });
    
    });
    <div id="quote"></div>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone-min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.min.js"></script>
    
    <script type="text/html" id="quote-template">
      <p>The author is :
        <%= author %>
      </p>
      <p>The content is :
        <%= quote %>
      </p>
    </script>