Single server call during backbone view rendering when multiple views are sharing same model

75 views Asked by At

I have a multiple instances of a view which share single instance of model among themselves.

During rendering of view, I want to call a function inside model which makes server call to fetch some data only once.

As these views are instances of same view, they all are triggering the function inside model. Hence making multiple server call.
Any idea how i can trigger this function inside model only once.

2

There are 2 answers

1
mikeapr4 On

Assuming you are calling fetch on the Model. This call will return the request (actually a jqXHR object). So a pattern which can be very useful is:

fetchOnce: function() {
    if (!this.fetchRequest || this.fetchRequest.readyState == 4 && this.fetchRequest.status >= 400) {
        this.fetchRequest = this.fetch.apply(this, arguments);
    }
    return this.fetchRequest;
},

This will save the request when fetch is called and avoid any additional calls while the current request is in-progress or if it has completed successfully.

Because the jqXHR object is a Deferred Promise object, anytime fetchOnce is called, callbacks can always be added (like deferred.done):

model.fetchOnce().done(function() { console.log('model fetched!'); });
0
Emile Bergeron On

Extending mikeapr4's answer, I made a simple model which overrides the fetch function to only fetch once (optionally every X hours).

It uses jQuery's Deferred .state() function to determine if the request is pending or completed.

Note that I used MomentJS to calculate time differences but it could be achieved easily with JavaScript native dates.

var FetchOnceModel = Backbone.Model.extend({
    fetchDelay: 8, // hours before forcing another fetch,
    /**
     * False if the promise is pending, or the last fetch was within the delay.
     * Force a new fetch if the lang has changed since the last fetch.
     * @return {Boolean} fetch is needed
     */
    isFetchDue: function() {
        var lastFetch = this.lastFetch,
            promise = this.promise,
            // use the jQuery deferred `state` function
            isPending = promise && promise.state() === "pending";

        return !isPending && !lastFetch || // not fetched yet?
            (this.fetchDelay && moment().diff(lastFetch, 'hours') > this.fetchDelay); // is delay passed?
    },

    fetch: function() {
        if (this.isFetchDue()) {
            this.promise = this.fetch({
                context: this,
                success: this._onSync,
                error: this._onError
            });
        }
        return this.promise;
    },

    _onSync: function() {
        this.lastFetch = moment();
        this.onSync.apply(this, arguments);
    },
    _onError: function() {
        this.lastFetch = null;
        this.onError.apply(this, arguments);
    },

    // left to override by the child model
    onError: _.noop,
    onSync: _.noop
});

Then it's transparent for the view and it can call fetch any number of times it want.

A simple view using it:

var View = Backbone.View.extend({
    initialize: function() {
        // use the model in case it has been already fetched
        this.useModel();
        // then fetch anyway to ensure it's fetched
        this.listenTo(this.model, 'sync', this.onModelSync);
        this.model.fetch();
    },
    useModel: function() {
        // ...use the model data, maybe render here.
    }
    onModelSync: function() {
        // things that need to be done only when model sync succeeded.
        this.useModel();
    }
});