Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should the backbone router or view handle fetching data and displaying loading status?

Tags:

backbone.js

In many places in my app the following pattern happens:

  • User clicks some link triggering navigation
  • Data needs to be fetched to render the view
  • UI design requires a "loading" spinner to be shown while data is fetched
  • Once the data is fetched we show the rendered view

I have tried both of the following implementation patterns:

  1. Router handles fetching

    • Router tells the container view to show the loading spinner
    • Router loads any collections/models
    • Router tells the container view to hide the loading spinner
    • Router passes the collections/models to the view and renders it
  2. View handles fetching

    • Router just creates and renders the view
    • The view fetches the collections and models it needs
    • When the view is first rendered, it just shows the loading spinner since the data is still loading
    • When the data arrives, the models/collections fire events and the view is bound to those so it re-renders itself, thus hiding the loading spinner and showing the full view

I dislike #1 since the router becomes a gigantic ball of Model/Collection fetching logic and seems to have too much responsibility. #2 seems like a better allocation of responsibilities (router just decides which view to show, view figures out what data it needs to fetch), but it does make the view rendering a little trickier since it's stateful now.

What does the StackOverflow community think? 1, 2, or something else?

like image 670
Peter Lyons Avatar asked May 29 '12 23:05

Peter Lyons


2 Answers

This post is pretty old, but we were reviewing it earlier today, so in case anyone else comes across it:

To me, I really see 2 separate questions:

  1. Where should the mechanics of data fetching and the resulting view rendering happen, in the router or a view?
  2. Should views expect already resolved models, or should they respond to models that may still be loading?

A bit of how we handle it mixed with some personal preferences:

  1. Neither, although I'd lean closer to the router.  Routers should handle routing, views should handle viewing, and something else should handle the mechanics and workflow of Model/Collection fetching logic.  We call that something else a Controller, which the Router basically delegates to.
  2. As Yuri alludes to, 'sometimes' is a reality.  I think this is probably a case by case decision, but should ultimately be a contract between a Controller and View, rather than between the Router/View.

I like Yuri's bullet points, with a couple caveats (indented bullets):

  • The router only knows where to send the user
  • The outer view only knows what the user should be viewing (given its data)
    • Assuming the outer view is specific to the inner view's use case and is 'owned' by another view (for clean up)
    • Otherwise for generic containers(like rendering into a 'main' location), we've found it useful to have a component that manages the views for a certain 'section' on the page - we call it a Renderer
  • The inner views only know how to show only their little piece of it all (and can be used elsewhere)
  • and The render function always shows the right thing as of right now.
    • In the case of a generic container, it'd ultimately be the responsibility of the Renderer

The main reason for the Renderer is to handle things related to that section, like cleaning up existing views to avoid ghost views, scrolling to the top on render (our MainContentRenderer does that), or showing a spinner in this case.

A psuedo-code-ish example of what that might look like, for:

  • a generic content target 'main' (if it's use case specific, may be better off with a ComponentView as per Yuri's example, depending on your view lifecycle management strategy)
  • a model we have to fetch and wait on
  • a view that accepts an already loaded model

Router:

routes: {
    "profile": "showProfile"
},

showProfile: function() {
    return new ProfileController().showProfile();
}

ProfileController:

showProfile: function() {
    //simple case
    var model = new Model();
    var deferredView = model.fetch.then(function() {
        return new View(model);
    };
    MainContentRenderer.renderDeferred(deferredView);
}

MainContentRenderer:

var currentView;

renderDeferred: function(deferredView) {
    showSpinner();
    deferredView.then(function(view) {
        this.closeSpinner();
        this.closeCurrentView();
        this.render(view);
    }
},

render: function(view) {
    currentView = view;
    $('#main-content').html(view.render().el);
}

closeCurrentView: function() {
    if (currentView and currentView.close()) {
        currentView.close();
    }
}

Introducing a Controller also has the added benefit of testability. For example, we have complex rules for performing searches around URL management, picking between a results view and a new search view, and picking between a cached 'last' search result and executing a new search. We have Jasmine tests for the controllers to verify that all that flow logic is correct. It also provides an isolated place to manage these rules.

like image 106
Kyle Winter Avatar answered Nov 02 '22 11:11

Kyle Winter


I tend to use the second option with three views, the container, a loading view, and a content view. That is, the container is instantiated by the router and during each render it looks at what it has on hand to display—sometimes provided by the router, sometimes by itself—and decides what view(s) to instantiate. A simplistic, contrived example:

ContainerView = Backbone.View.extend({

  initialize: function (options) {
    options.data.bind("reset", this.render, this);
  },

  render: function () {
    var view;

    // If the loading view is only shown once, e.g., splashing, then isReady()
    // may be better here.
    if (this.options.data.isLoading()) {
      view = LoadingView;
    } else {
      view = DataView;
    }

    this.$("div.content").html(new view().render().el);
  }

});

I like this approach because:

  • The router only knows where to send the user;
  • The outer view only knows what the user should be viewing (given its data);
  • The inner views only know how to show only their little piece of it all (and can be used elsewhere); and
  • The render function always shows the right thing as of right now.

Clarification: The purpose of the view, in this case, is to understand how what is has to show should best be shown to the user. In this case, a bit of data still loading is best shown with a loading view, while ready data is best shown with a data view. Most real views are actually composing their display with many more views, e.g., depending on the user authorization different action containers.

like image 40
Yuri Gadow Avatar answered Nov 02 '22 10:11

Yuri Gadow